Compare commits
229
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2955d6607 | ||
|
|
333d118c0f | ||
|
|
4c8dc39f43 | ||
|
|
d40502aa3b | ||
|
|
4007b20fc2 | ||
|
|
3e4d4d93fd | ||
|
|
0f77ada55e | ||
|
|
9e30d93af3 | ||
|
|
c30878f616 | ||
|
|
6b7923cdd2 | ||
|
|
2df6b51ebb | ||
|
|
4200a425d5 | ||
|
|
9975bd23cd | ||
|
|
5b28a8b331 | ||
|
|
056c123759 | ||
|
|
4a5673e1dc | ||
|
|
385445804a | ||
|
|
bcfb4072f2 | ||
|
|
23c4eba5b3 | ||
|
|
80777d96da | ||
|
|
2fd11961cf | ||
|
|
55f0267c40 | ||
|
|
488fd5f60b | ||
|
|
d8c18f604f | ||
|
|
9c28f3c4c0 | ||
|
|
a8195ba2cc | ||
|
|
31ed005183 | ||
|
|
6a431d806a | ||
|
|
9d1c3631b8 | ||
|
|
405732a7f5 | ||
|
|
4fbc501121 | ||
|
|
0356f7677f | ||
|
|
b7bc855492 | ||
|
|
726b730b84 | ||
|
|
b7539467a6 | ||
|
|
7ee3f236a9 | ||
|
|
9dd72c4911 | ||
|
|
4ecc7a72f2 | ||
|
|
ed933d1893 | ||
|
|
fa26affc86 | ||
|
|
8097ec456a | ||
|
|
f570d40f50 | ||
|
|
bb6a58c2a7 | ||
|
|
6ee87fbf2d | ||
|
|
a6d9e49ab4 | ||
|
|
eec75ce918 | ||
|
|
4f4cc59873 | ||
|
|
dcb036292e | ||
|
|
9fca6c0296 | ||
|
|
aa6eee1a71 | ||
|
|
c7e207c290 | ||
|
|
b5c0b5b53e | ||
|
|
4ec2343d91 | ||
|
|
21241d2323 | ||
|
|
4593c29b64 | ||
|
|
a3920e2abd | ||
|
|
1b0ab75a84 | ||
|
|
3afcd64146 | ||
|
|
b783e7febe | ||
|
|
60c110d97b | ||
|
|
9bd7e2cb45 | ||
|
|
d6e545cc48 | ||
|
|
9240bd8d16 | ||
|
|
ea853cebde | ||
|
|
9ddf22e196 | ||
|
|
56e2bf7696 | ||
|
|
41681742cc | ||
|
|
a5797c5be6 | ||
|
|
5f73cb061b | ||
|
|
6c68ff565f | ||
|
|
dfad09e3b5 | ||
|
|
3b64c813cf | ||
|
|
acf33d6df2 | ||
|
|
ecf3ed8bdb | ||
|
|
4ad937c6ca | ||
|
|
2f08c7c932 | ||
|
|
36aeec9586 | ||
|
|
5dd3b4655e | ||
|
|
4cea84d7cb | ||
|
|
9aec6908b5 | ||
|
|
60cb68a276 | ||
|
|
271b1620e6 | ||
|
|
2a6eb41147 | ||
|
|
c34efca4cd | ||
|
|
3f547eafa4 | ||
|
|
7eb1a9500c | ||
|
|
6740ed5215 | ||
|
|
abf7c1142e | ||
|
|
20152a9872 | ||
|
|
0e2738ffb6 | ||
|
|
9d1f88cbe8 | ||
|
|
6221989f89 | ||
|
|
f8bb113c22 | ||
|
|
f2dc749c86 | ||
|
|
c523c38027 | ||
|
|
e80fe414b1 | ||
|
|
e8c56e0c0a | ||
|
|
fa2b6c5a61 | ||
|
|
34d00fd323 | ||
|
|
2f604a3c3a | ||
|
|
a6d5b3ea9d | ||
|
|
e1d755f7cd | ||
|
|
3fd96506f2 | ||
|
|
79ea571512 | ||
|
|
7b3c797237 | ||
|
|
8c24272ee7 | ||
|
|
fe342faa83 | ||
|
|
33b20991c4 | ||
|
|
f2bbd5957e | ||
|
|
bf9cce1aef | ||
|
|
c2a6f4bc43 | ||
|
|
1860d4f0e5 | ||
|
|
bedd55a4b4 | ||
|
|
24e1fa9a76 | ||
|
|
53db3a476b | ||
|
|
b3aca5289b | ||
|
|
a8c58c6605 | ||
|
|
f55c2b9267 | ||
|
|
bd0e341572 | ||
|
|
7ba3580c86 | ||
|
|
dca293a446 | ||
|
|
23e56c517c | ||
|
|
f3645aa51e | ||
|
|
7acac615ca | ||
|
|
0fe96d92df | ||
|
|
a66e43a99d | ||
|
|
5457f4a2bc | ||
|
|
9e7605482a | ||
|
|
b3529aa327 | ||
|
|
f580d9d073 | ||
|
|
593a8e12ce | ||
|
|
0ae3c0837b | ||
|
|
a2a3243ea8 | ||
|
|
07ff8b30ee | ||
|
|
81996df7a5 | ||
|
|
0d96275b68 | ||
|
|
2160d4fae5 | ||
|
|
c490d85045 | ||
|
|
07888ec096 | ||
|
|
d4dc940bd8 | ||
|
|
d65730c4c2 | ||
|
|
e91c8c39c2 | ||
|
|
344b9b6dff | ||
|
|
bf04d1aea3 | ||
|
|
8b62b1a715 | ||
|
|
1783bf6dfd | ||
|
|
ed4a62871f | ||
|
|
b8a9da0c0f | ||
|
|
6901edc05d | ||
|
|
8f229ede70 | ||
|
|
51559245d2 | ||
|
|
84cc819284 | ||
|
|
6838b6bff9 | ||
|
|
960b4df056 | ||
|
|
a83dbb2fb3 | ||
|
|
71f03a7224 | ||
|
|
7b6f7becd2 | ||
|
|
1f8ad7bc68 | ||
|
|
7f46bb1824 | ||
|
|
dae2b9cc4e | ||
|
|
48053d1032 | ||
|
|
f0d8cdafa8 | ||
|
|
97f6a0f3fd | ||
|
|
92a1c33625 | ||
|
|
8ccdb33dd2 | ||
|
|
788f4057ae | ||
|
|
f07acde513 | ||
|
|
f47ee1abb4 | ||
|
|
935721df10 | ||
|
|
3c42403b78 | ||
|
|
a8161f0e55 | ||
|
|
513d9b1555 | ||
|
|
7f2348619a | ||
|
|
ede91ff1f0 | ||
|
|
f3b3fb07de | ||
|
|
ab4193c1f4 | ||
|
|
c824db1ab7 | ||
|
|
17d7dbc461 | ||
|
|
2a45d8de7d | ||
|
|
ba098f24e5 | ||
|
|
1712e6d821 | ||
|
|
3e1e8cd3f8 | ||
|
|
59d465971e | ||
|
|
281e41f766 | ||
|
|
0f96bf365c | ||
|
|
969519c00f | ||
|
|
222ce83967 | ||
|
|
7929bf1688 | ||
|
|
8ab40974d3 | ||
|
|
91fb87d17e | ||
|
|
54153ed62c | ||
|
|
f942558573 | ||
|
|
1fb22b4c21 | ||
|
|
1a75f8dc23 | ||
|
|
18624df572 | ||
|
|
0ef1b91cf3 | ||
|
|
64f64fec30 | ||
|
|
4602efd27c | ||
|
|
94e6600faf | ||
|
|
4d228b4d8c | ||
|
|
a5e355a3d5 | ||
|
|
fdeeb27f23 | ||
|
|
d5d2e56d04 | ||
|
|
d7a07535b0 | ||
|
|
6c03f83b1b | ||
|
|
4e0127d620 | ||
|
|
6fa6f9b6d9 | ||
|
|
3f336fefb1 | ||
|
|
4ee30e4145 | ||
|
|
645668b70d | ||
|
|
ba3151891e | ||
|
|
c4cd0330e0 | ||
|
|
1caa0d6e54 | ||
|
|
17c8b4c2f1 | ||
|
|
58c749e831 | ||
|
|
8ec9bb1bfe | ||
|
|
7157ab0b70 | ||
|
|
fecddc0549 | ||
|
|
6d37efe8fa | ||
|
|
90bb1610f5 | ||
|
|
1a0931b630 | ||
|
|
40bf55b74c | ||
|
|
b2f7561bdd | ||
|
|
f2c4edd2bf | ||
|
|
4716fa2ece | ||
|
|
521139d4a5 | ||
|
|
5184816199 | ||
|
|
9522adf19a | ||
|
|
7a43420029 |
@@ -1,39 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug in REA
|
||||
title: "[Bug] "
|
||||
labels: ["bug", "triage"]
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## Bug Description
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
What you expected to happen.
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
What actually happened, including any error messages or unexpected output.
|
||||
|
||||
## Environment
|
||||
|
||||
- REA version (`rea --version` or `npm list @morluto/rea`):
|
||||
- Node.js version (`node --version`):
|
||||
- macOS version:
|
||||
- Hopper Disassembler version:
|
||||
- Target binary format (Mach-O, ELF, PE, .hop):
|
||||
|
||||
## Additional Context
|
||||
|
||||
- MCP client and version (if applicable):
|
||||
- Any relevant logs or stack traces:
|
||||
- Does the issue reproduce with a different target binary?
|
||||
@@ -0,0 +1,155 @@
|
||||
name: Bug report
|
||||
description: Report a reproducible problem in REA
|
||||
title: "[Bug] "
|
||||
labels:
|
||||
- bug
|
||||
- triage
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please report one primary behavior per issue. Keep observed behavior
|
||||
separate from suspected causes, and remove credentials and private paths.
|
||||
|
||||
- type: checkboxes
|
||||
id: before-submitting
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I searched the open and closed issues for a related report.
|
||||
required: true
|
||||
- label: I removed credentials, tokens, private paths, and sensitive target data.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
options:
|
||||
- Setup, packaging, or upgrade
|
||||
- Provider or capability discovery
|
||||
- Process capture or runtime observation
|
||||
- Native analysis or decompilation
|
||||
- Evidence, artifacts, or reconstruction
|
||||
- CLI
|
||||
- MCP or agent integration
|
||||
- Documentation
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: observed-behavior
|
||||
attributes:
|
||||
label: Observed behavior
|
||||
description: What happened? Include the command, MCP tool, provider, target type, and conditions that matter.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction
|
||||
description: Give the smallest realistic command, configuration, input, or event sequence that demonstrates the problem.
|
||||
placeholder: |
|
||||
1. Configure or start ...
|
||||
2. Run ...
|
||||
3. Observe ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What result, contract, invariant, recovery action, or state transition should have occurred?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: impact-scope
|
||||
attributes:
|
||||
label: Impact and scope
|
||||
description: What is affected, what is not affected, and does this affect evidence integrity, process isolation, compatibility, or recovery?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: rea-version
|
||||
attributes:
|
||||
label: REA version
|
||||
description: Use `rea --version` or provide the source revision.
|
||||
placeholder: 3.1.0 or git revision
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: runtime
|
||||
attributes:
|
||||
label: Runtime and platform
|
||||
description: Include Node.js, OS, architecture, and Hopper/Ghidra version when relevant.
|
||||
placeholder: Node 24.18; macOS 15 arm64; Hopper 5.15
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: target
|
||||
attributes:
|
||||
label: Target and artifact context
|
||||
description: Include binary format, app/container type, artifact kind, or workload details when relevant.
|
||||
placeholder: Mach-O arm64 app; Evidence v2 bundle; not applicable
|
||||
|
||||
- type: dropdown
|
||||
id: interface
|
||||
attributes:
|
||||
label: Interface
|
||||
options:
|
||||
- CLI
|
||||
- MCP
|
||||
- Python bridge
|
||||
- Package or setup
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: failure-stage
|
||||
attributes:
|
||||
label: Earliest failure stage
|
||||
description: Select where the incorrect behavior first becomes observable.
|
||||
options:
|
||||
- Request parsing or validation
|
||||
- Setup or provider admission
|
||||
- Capture or observation
|
||||
- Artifact or evidence registration
|
||||
- Native analysis or reconstruction
|
||||
- Transport or result projection
|
||||
- Cleanup, cancellation, or lifecycle
|
||||
- Unknown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: relevant-evidence
|
||||
attributes:
|
||||
label: Relevant output or evidence
|
||||
description: Paste the smallest useful CLI output, MCP result, error, trace, or evidence excerpt. Redact sensitive values.
|
||||
render: text
|
||||
|
||||
- type: textarea
|
||||
id: typed-outcome
|
||||
attributes:
|
||||
label: Typed outcome or recovery expected
|
||||
description: Optional. What typed result, error, next tool, cleanup state, or limitation should the caller have received?
|
||||
|
||||
- type: textarea
|
||||
id: analysis
|
||||
attributes:
|
||||
label: Analysis or suspected cause
|
||||
description: Optional. Clearly mark source-derived reasoning or a hypothesis; do not present an unverified cause as fact.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add links, reduced fixtures, screenshots, or a proposed regression test.
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Design discussion
|
||||
description: Resolve an REA architecture, provider, or contract question before implementation
|
||||
title: "[Design] "
|
||||
labels:
|
||||
- enhancement
|
||||
- triage
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Use this for a substantial provider, public/persisted contract, or
|
||||
authority-boundary decision. Keep one decision per discussion.
|
||||
|
||||
- type: checkboxes
|
||||
id: before-submitting
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I searched the open and closed issues and pull requests for a related discussion.
|
||||
required: true
|
||||
- label: I removed credentials, tokens, private paths, and sensitive target data.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: decision
|
||||
attributes:
|
||||
label: Decision needed
|
||||
description: State the exact question maintainers need to resolve.
|
||||
placeholder: Should REA ... or ...?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context-evidence
|
||||
attributes:
|
||||
label: Context and evidence
|
||||
description: Explain why the decision is needed now. Separate observed behavior, derived evidence, and inference.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: boundary-invariants
|
||||
attributes:
|
||||
label: Boundary and invariants
|
||||
description: Identify the owning layer, provider/bridge boundary, artifact or evidence invariants, and any process or authorization constraints.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: options
|
||||
attributes:
|
||||
label: Options and tradeoffs
|
||||
description: Compare a small number of viable approaches, including platform, compatibility, safety, evidence, and maintenance costs.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: success-criteria
|
||||
attributes:
|
||||
label: Success criteria and open questions
|
||||
description: State what outcome would settle the decision and list only unresolved questions that could change the choice.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: references
|
||||
attributes:
|
||||
label: References
|
||||
description: Link relevant contracts, upstream provider documentation, tests, measurements, or prior discussions.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: Documentation
|
||||
about: Suggest improvements to REA documentation
|
||||
title: "[Docs] "
|
||||
labels: ["documentation", "triage"]
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## Documentation Area
|
||||
|
||||
Which documentation needs improvement? (README, AGENTS.md, API docs, inline JSDoc, skill files)
|
||||
|
||||
## Problem
|
||||
|
||||
What is unclear, missing, or incorrect in the current documentation?
|
||||
|
||||
## Suggested Improvement
|
||||
|
||||
Describe the change you would like to see.
|
||||
|
||||
## Additional Context
|
||||
|
||||
- Link to the relevant documentation file or section:
|
||||
- Any examples or references that would help:
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Documentation
|
||||
description: Report unclear, missing, or incorrect REA documentation
|
||||
title: "[Docs] "
|
||||
labels:
|
||||
- documentation
|
||||
- triage
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: before-submitting
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I searched the existing issues for a related documentation report.
|
||||
required: true
|
||||
- label: I removed credentials, tokens, private paths, and sensitive target data.
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: location
|
||||
attributes:
|
||||
label: Documentation location
|
||||
description: Link the page, section, command, API, skill, or inline comment.
|
||||
placeholder: README.md#quick-start or docs/mcp-contracts.md
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: What is unclear, missing, outdated, or incorrect, and who is affected?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-content
|
||||
attributes:
|
||||
label: Expected content
|
||||
description: Describe the accurate behavior or example that should be documented.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: evidence
|
||||
attributes:
|
||||
label: Supporting evidence
|
||||
description: Add source, command output, provider documentation, or a link that supports the correction.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Include screenshots, affected translations, or related issues.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Propose a new feature or enhancement for REA
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement", "triage"]
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
What problem does this feature solve? Describe the use case and why existing tools are insufficient.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
Describe the feature or enhancement you would like to see. Include:
|
||||
|
||||
- Which REA tool(s) this affects (direct, enhanced, or session tools)
|
||||
- Expected inputs and outputs
|
||||
- How it integrates with the existing MCP workflow
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
Any alternative approaches or workarounds you have tried.
|
||||
|
||||
## Additional Context
|
||||
|
||||
- Relevant Hopper API methods or concepts:
|
||||
- Links to related issues or discussions:
|
||||
- Would this require changes to the bridge (`bridge/hopper_bridge.py`)?
|
||||
@@ -0,0 +1,88 @@
|
||||
name: Feature request
|
||||
description: Propose a focused improvement to REA
|
||||
title: "[Feature] "
|
||||
labels:
|
||||
- enhancement
|
||||
- triage
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Keep one primary capability or workflow outcome per request. For a
|
||||
substantial provider, public contract, or architecture change, explain
|
||||
the decision maintainers need to make before implementation.
|
||||
|
||||
- type: checkboxes
|
||||
id: before-submitting
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I searched the open and closed issues and pull requests for a related proposal.
|
||||
required: true
|
||||
- label: I removed credentials, tokens, private paths, and sensitive target data.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
options:
|
||||
- Setup, packaging, or upgrade
|
||||
- Provider or capability discovery
|
||||
- Process capture or runtime observation
|
||||
- Native analysis or decompilation
|
||||
- Evidence, artifacts, or reconstruction
|
||||
- CLI
|
||||
- MCP or agent integration
|
||||
- Documentation
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: current-limitation
|
||||
attributes:
|
||||
label: Current limitation
|
||||
description: What user or agent workflow is blocked, ambiguous, or unnecessarily difficult today?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: use-cases
|
||||
attributes:
|
||||
label: Use cases
|
||||
description: Give concrete applications, targets, callers, or investigation workflows. Explain why current tools are insufficient.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: desired-outcome
|
||||
attributes:
|
||||
label: Desired outcome and success criteria
|
||||
description: Describe observable behavior, inputs, outputs, evidence, and compatibility criteria that would make this useful.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: boundaries
|
||||
attributes:
|
||||
label: Authority boundaries and non-goals
|
||||
description: Call out process execution, target mutation, provider licensing, evidence provenance, supported platforms, and work that should remain out of scope.
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: Describe existing REA operations, upstream tools, or workarounds considered and why they do not meet the need.
|
||||
|
||||
- type: textarea
|
||||
id: possible-direction
|
||||
attributes:
|
||||
label: Possible direction
|
||||
description: Optional implementation or integration idea. Treat it as a proposal, not a requirement.
|
||||
|
||||
- type: textarea
|
||||
id: references
|
||||
attributes:
|
||||
label: References
|
||||
description: Link relevant provider documentation, source, measurements, artifacts, issues, or discussions.
|
||||
@@ -1,34 +1,73 @@
|
||||
## Description
|
||||
<!--
|
||||
PR title: type(optional-scope): imperative outcome
|
||||
The title becomes the squash-merge commit subject and generated changelog entry.
|
||||
-->
|
||||
|
||||
<!-- What does this change do? Why is it needed? -->
|
||||
## Summary
|
||||
|
||||
## Type of Change
|
||||
<!-- What user, agent, or maintainer problem does this solve? Link the issue
|
||||
with "Fixes #123" when applicable. Keep the prior behavior, expected contract,
|
||||
and new behavior understandable without private context. -->
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Documentation update
|
||||
- [ ] Performance improvement
|
||||
- [ ] Refactoring (no functional change)
|
||||
- [ ] Tool contract change (MCP schema modification)
|
||||
## Problem and expected behavior
|
||||
|
||||
## Testing Performed
|
||||
<!-- For fixes, give the smallest trigger and violated invariant. For features,
|
||||
describe the use case and observable outcome. -->
|
||||
|
||||
<!-- Describe the testing you have done to verify this change. -->
|
||||
## Change and scope
|
||||
|
||||
- [ ] Unit tests added or updated
|
||||
- [ ] TypeScript typecheck passes (`npm run typecheck`)
|
||||
- [ ] Linting passes (`npm run lint`)
|
||||
- [ ] Full test suite passes (`npm test`)
|
||||
- [ ] Real Hopper verification performed (`npm run verify:hopper`)
|
||||
- [ ] Linux real Hopper verification performed (`npm run verify:hopper:linux`) or unavailability explained
|
||||
- [ ] Real browser verification performed (`npm run verify:browser`) or unavailability explained
|
||||
- [ ] Package verification performed (`npm run verify:package`)
|
||||
<!-- Explain the chosen approach, why it fits REA's architecture, and what is
|
||||
intentionally not included. -->
|
||||
|
||||
## Tool Contract Impact
|
||||
## Contract and boundary impact
|
||||
|
||||
<!-- If this PR adds, removes, or modifies MCP tools, list them here. -->
|
||||
<!-- Canonical inventory and family counts are generated in docs/product-catalog.json. -->
|
||||
<!-- Complete the applicable lines; use "none" or "not applicable" explicitly. -->
|
||||
|
||||
## Related Issues
|
||||
- Semantic owner and earliest changed stage:
|
||||
- CLI and MCP/tool-catalog contract:
|
||||
- Provider, bridge, target-format, or platform compatibility:
|
||||
- Evidence, artifact, provenance, or reconstruction contract:
|
||||
- Process execution, authorization, cleanup, or containment impact:
|
||||
- Generated metadata (`docs/product-catalog.json`), package, or installation impact:
|
||||
|
||||
<!-- Link issues this PR closes or relates to. Use "Closes #123" syntax. -->
|
||||
## Evidence and regression coverage
|
||||
|
||||
<!-- State whether evidence is executed, source-derived, or proposed. For fixes,
|
||||
explain how the regression would fail on the affected base revision when practical. -->
|
||||
|
||||
- Tests added or updated:
|
||||
- Base reproduction or other evidence:
|
||||
- User-visible CLI/MCP output (if applicable):
|
||||
- Remaining proof gaps:
|
||||
|
||||
For evidence-bearing changes:
|
||||
|
||||
- [ ] Observed, derived, and inferred claims remain distinguishable.
|
||||
- [ ] Artifact identity, source provenance, and failed attempts remain preserved.
|
||||
- [ ] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.
|
||||
|
||||
## Validation performed
|
||||
|
||||
<!-- List only commands that actually ran, with observed results. Include the
|
||||
relevant real-provider or platform workflow, or explain why it was unavailable. -->
|
||||
|
||||
- `command` — result
|
||||
|
||||
## Compatibility, safety, and release
|
||||
|
||||
<!-- Call out breaking changes, migrations, package effects, platform/provider
|
||||
limits, security/privacy/containment implications, and meaningful proof gaps. -->
|
||||
|
||||
- Breaking changes or migration steps:
|
||||
- Real Hopper/Ghidra, browser, or OS coverage:
|
||||
- Package or release metadata impact:
|
||||
- Security, privacy, process, or containment review:
|
||||
|
||||
## Review checklist
|
||||
|
||||
- [ ] The PR has one focused outcome and the title follows `type(scope): outcome`.
|
||||
- [ ] Related issue is linked, or the reason for not linking one is stated above.
|
||||
- [ ] Tests cover changed observable behavior and meaningful failure paths.
|
||||
- [ ] Owning docs, contracts, and generated metadata are updated where needed.
|
||||
- [ ] User-visible CLI/MCP changes include representative output.
|
||||
- [ ] I checked the final diff for secrets, unrelated cleanup, and unsupported claims.
|
||||
|
||||
+97
-20
@@ -88,7 +88,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1/2, 2/2]
|
||||
shard: [1/4, 2/4, 3/4, 4/4]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
include-hidden-files: true
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 1
|
||||
retention-days: 7
|
||||
|
||||
build:
|
||||
name: Build package
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
name: rea-dist
|
||||
path: dist
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
retention-days: 7
|
||||
|
||||
test:
|
||||
name: Test (coverage + timing)
|
||||
@@ -160,18 +160,63 @@ jobs:
|
||||
path: .vitest-reports
|
||||
pattern: vitest-report-*
|
||||
merge-multiple: true
|
||||
- if: needs.changes.outputs.code == 'true' && success()
|
||||
run: npm run test:ci:merge
|
||||
- name: Merge coverage, JUnit, and timing reports
|
||||
if: needs.changes.outputs.code == 'true' && success()
|
||||
run: >-
|
||||
npm run test:ci:merge --
|
||||
--coverage.reportsDirectory=coverage
|
||||
--coverage.reporter=text
|
||||
--coverage.reporter=json-summary
|
||||
--coverage.reporter=lcov
|
||||
--reporter=default
|
||||
--reporter=junit
|
||||
--reporter=json
|
||||
--outputFile.junit=test-results.xml
|
||||
--outputFile.json=test-results.json
|
||||
- name: Summarize slowest test files
|
||||
if: needs.changes.outputs.code == 'true' && success()
|
||||
shell: bash
|
||||
run: |
|
||||
node <<'NODE' >> "${GITHUB_STEP_SUMMARY}"
|
||||
const report = JSON.parse(require("node:fs").readFileSync("test-results.json", "utf8"));
|
||||
const files = (report.testResults ?? [])
|
||||
.map((result) => ({
|
||||
name: result.name,
|
||||
duration: Math.max(0, (result.endTime ?? result.startTime) - result.startTime),
|
||||
}))
|
||||
.sort((left, right) => right.duration - left.duration)
|
||||
.slice(0, 20);
|
||||
console.log("## Slowest deterministic test files\n");
|
||||
console.log("| File | Duration (ms) |");
|
||||
console.log("| --- | ---: |");
|
||||
for (const file of files) {
|
||||
console.log(`| \`${file.name}\` | ${file.duration} |`);
|
||||
}
|
||||
NODE
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: needs.changes.outputs.code == 'true' && always()
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results.xml
|
||||
path: |
|
||||
test-results.xml
|
||||
test-results.json
|
||||
coverage/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
conformance-fixtures:
|
||||
name: Source-built conformance fixtures
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Verify fixtures from a clean checkout
|
||||
run: npm run verify:fixtures
|
||||
|
||||
package-e2e:
|
||||
name: Package E2E (${{ matrix.os }})
|
||||
name: Package/install integration (${{ matrix.os }})
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
strategy:
|
||||
@@ -188,6 +233,24 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run verify:package
|
||||
|
||||
apple-artifacts:
|
||||
name: Real Apple artifact CLI/MCP E2E
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run verify:keyed-archive
|
||||
- run: npm run verify:asset-catalog
|
||||
- name: Real binary metadata and Interface Builder integration
|
||||
run: npm run verify:apple-dispatch && npm run verify:interface-builder
|
||||
|
||||
reconstruction-readiness:
|
||||
name: Reconstruction readiness
|
||||
needs: changes
|
||||
@@ -234,7 +297,7 @@ jobs:
|
||||
run: npm run verify:inspector
|
||||
|
||||
windows-curated:
|
||||
name: Windows Ghidra P0 and package lane
|
||||
name: Windows capability and package lane
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
runs-on: windows-latest
|
||||
@@ -255,20 +318,34 @@ jobs:
|
||||
node -e "const report = JSON.parse(require('node:fs').readFileSync('windows-capabilities.json', 'utf8'));
|
||||
if (report.platform !== 'win32' || report.architecture !== 'x64')
|
||||
throw new Error('expected a Windows x64 capability report');"
|
||||
- name: Run curated Windows P0 tests
|
||||
# Keep this list aligned with tests/process-global/curatedWindowsLane.test.ts,
|
||||
# which fails when any path below is renamed or removed. Vitest silently
|
||||
# ignores unmatched paths, so a stale entry would otherwise report green
|
||||
# while skipping coverage.
|
||||
#
|
||||
# Every path here must pass on windows-latest WITHOUT verified Windows
|
||||
# native authority. src/ghidra/GhidraProvider.test.ts,
|
||||
# src/ghidra/GhidraProvider.behavior.test.ts successors, and
|
||||
# tests/boundary/process/providerProcess.test.ts are deliberately absent:
|
||||
# they require Job Object process ownership and private-runtime DACL
|
||||
# enforcement, which this runner does not provide by design. Those claims
|
||||
# belong behind the same approval boundary as the real Ghidra Windows
|
||||
# workflow, not in the default lane.
|
||||
- name: Run curated Windows capability tests
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
tests/binaryTarget.test.ts
|
||||
tests/windowsCapabilities.test.ts
|
||||
tests/ghidraWindowsFixture.test.ts
|
||||
tests/ghidraInstallation.test.ts
|
||||
tests/ghidraProvider.test.ts
|
||||
tests/ghidraTransport.test.ts
|
||||
tests/ghidraTargetSnapshot.test.ts
|
||||
tests/ghidraClient.test.ts
|
||||
tests/ghidraLauncher.test.ts
|
||||
tests/processOwnership.test.ts
|
||||
tests/realGhidraWindowsWorkflow.test.ts
|
||||
tests/boundary/filesystem/binaryTarget.test.ts
|
||||
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
|
||||
tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts
|
||||
src/ghidra/GhidraInstallation.test.ts
|
||||
tests/boundary/providers/ghidra/ghidraTransport.test.ts
|
||||
tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts
|
||||
tests/boundary/providers/ghidra/ghidraLauncher.test.ts
|
||||
src/process/ProcessOwnership.test.ts
|
||||
src/process/ProcessOwnership.part2.test.ts
|
||||
src/process/ProcessOwnership.part3.test.ts
|
||||
src/process/ProcessOwnership.validation.test.ts
|
||||
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
|
||||
- run: npm run verify:package:windows
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
|
||||
@@ -12,7 +12,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Real Ghidra 12.1.2 Windows x64 PE proof
|
||||
name: Real Ghidra 12.1.4 Windows x64 PE proof
|
||||
if: github.ref == 'refs/heads/main'
|
||||
environment: real-ghidra-windows
|
||||
runs-on: [self-hosted, Windows, x64, ghidra-12-1-2]
|
||||
|
||||
@@ -25,7 +25,9 @@ jobs:
|
||||
- uses: googleapis/release-please-action@v4
|
||||
id: release
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# A dedicated credential lets release-PR updates trigger CI. GitHub
|
||||
# requires manual approval for pull_request events from GITHUB_TOKEN.
|
||||
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check out release pull request
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
@@ -53,9 +55,22 @@ jobs:
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_PR_BRANCH: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
|
||||
# Use the same identity for the follow-up commit so it also triggers CI.
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
RELEASE_PR_JSON: ${{ steps.release.outputs.pr }}
|
||||
run: |
|
||||
release_pr_branch="$(node --input-type=module -e '
|
||||
const value = JSON.parse(process.env.RELEASE_PR_JSON ?? "");
|
||||
if (
|
||||
typeof value !== "object" ||
|
||||
value === null ||
|
||||
typeof value.headBranchName !== "string" ||
|
||||
value.headBranchName.length === 0
|
||||
) {
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(value.headBranchName);
|
||||
')"
|
||||
changed_paths="$(git diff --name-only)"
|
||||
if [[ -z "${changed_paths}" ]]; then
|
||||
echo "Release documentation is already canonical."
|
||||
@@ -72,7 +87,7 @@ jobs:
|
||||
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}"
|
||||
git push origin "HEAD:${release_pr_branch}"
|
||||
|
||||
publish:
|
||||
name: Publish to npm
|
||||
@@ -97,7 +112,15 @@ jobs:
|
||||
run: npm run verify:package
|
||||
|
||||
- name: Publish
|
||||
run: npm publish --access public
|
||||
shell: bash
|
||||
run: |
|
||||
version="$(node --input-type=module -e 'import packageJson from "./package.json" with { type: "json" }; process.stdout.write(packageJson.version)')"
|
||||
published_version="$(npm view "rea-agents@${version}" version --json 2>/dev/null || true)"
|
||||
if [ "${published_version}" = "\"${version}\"" ]; then
|
||||
echo "rea-agents@${version} is already published; verifying the existing package."
|
||||
else
|
||||
npm publish --access public
|
||||
fi
|
||||
|
||||
- name: Verify published CLI from npm
|
||||
shell: bash
|
||||
@@ -106,17 +129,59 @@ jobs:
|
||||
verification_root="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${verification_root}"' EXIT
|
||||
cd "${verification_root}"
|
||||
for attempt in 1 2 3 4 5 6; do
|
||||
if npm view "rea-agents@${version}" version --json >/dev/null 2>&1 \
|
||||
&& npm exec --yes --package="rea-agents@${version}" -- rea --help \
|
||||
&& npm exec --yes --package="rea-agents@${version}" -- rea --llms \
|
||||
&& node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"; then
|
||||
exit 0
|
||||
for attempt in $(seq 1 20); do
|
||||
published_version="$(npm view "rea-agents@${version}" version --json 2>/dev/null || true)"
|
||||
if [ "${published_version}" = "\"${version}\"" ]; then
|
||||
if npm exec --yes --package="rea-agents@${version}" -- rea --help \
|
||||
&& npm exec --yes --package="rea-agents@${version}" -- rea --llms \
|
||||
&& node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Published rea-agents@${version} is visible, but CLI verification failed on attempt ${attempt}" >&2
|
||||
else
|
||||
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/20)" >&2
|
||||
fi
|
||||
if [ "${attempt}" -lt 6 ]; then
|
||||
echo "Published package verification attempt ${attempt} failed; retrying in 10 seconds" >&2
|
||||
sleep 10
|
||||
if [ "${attempt}" -lt 20 ]; then
|
||||
sleep 15
|
||||
fi
|
||||
done
|
||||
echo "Published rea-agents@${version} failed registry and CLI verification" >&2
|
||||
exit 1
|
||||
|
||||
publish-mcp:
|
||||
name: Publish to MCP Registry
|
||||
needs: [release-please, publish]
|
||||
if: needs.release-please.outputs.release_created == 'true' && needs.publish.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.release-please.outputs.tag_name }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify package and registry metadata versions
|
||||
run: npm run mcp-registry:check
|
||||
|
||||
- name: Install MCP Registry publisher
|
||||
env:
|
||||
MCP_PUBLISHER_VERSION: v1.8.0
|
||||
MCP_PUBLISHER_SHA256_LINUX_AMD64: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
|
||||
run: |
|
||||
archive="mcp-publisher_linux_amd64.tar.gz"
|
||||
curl -fsSLo "${archive}" \
|
||||
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/${archive}"
|
||||
echo "${MCP_PUBLISHER_SHA256_LINUX_AMD64} ${archive}" | sha256sum --check --strict
|
||||
tar xzf "${archive}" mcp-publisher
|
||||
chmod +x mcp-publisher
|
||||
|
||||
- name: Validate MCP Registry metadata
|
||||
run: ./mcp-publisher validate
|
||||
|
||||
- name: Authenticate to MCP Registry
|
||||
run: ./mcp-publisher login github-oidc
|
||||
|
||||
- name: Publish MCP Registry metadata
|
||||
run: ./mcp-publisher publish
|
||||
|
||||
+7
-3
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"threshold": 3,
|
||||
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.6% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
|
||||
"threshold": 2.7,
|
||||
"reporters": ["consoleFull"],
|
||||
"ignore": [
|
||||
"node_modules/**",
|
||||
@@ -7,8 +8,11 @@
|
||||
"docs/**",
|
||||
".codex/**",
|
||||
".devin/**",
|
||||
"**/*.test.ts",
|
||||
"tests/fixtures/**"
|
||||
"tests/fixtures/**",
|
||||
"tests/conformance/**/*.c",
|
||||
"tests/conformance/**/*.m",
|
||||
"tests/conformance/**/*.swift",
|
||||
"tests/conformance/**/*.xib"
|
||||
],
|
||||
"gitignore": true,
|
||||
"minLines": 5,
|
||||
|
||||
+94
-3
@@ -18,10 +18,101 @@
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["tests/**/*.ts"],
|
||||
"files": [
|
||||
"src/{contracts,domain,server}/**/*.ts",
|
||||
"src/application/**/*.ts"
|
||||
],
|
||||
"excludeFiles": ["src/application/runtime.ts"],
|
||||
"rules": {
|
||||
"max-lines-per-function": "off",
|
||||
"max-lines": "off"
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["**/ghidra/**", "**/hopper/**"],
|
||||
"message": "Deep-provider imports belong behind adapters or in the application composition root."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
|
||||
"rules": {
|
||||
"max-lines": "off",
|
||||
"max-lines-per-function": [
|
||||
"error",
|
||||
{ "max": 150, "skipBlankLines": true, "skipComments": true }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/domain/**/*.{fixture,test}.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"**/application/**",
|
||||
"**/server/**",
|
||||
"**/process/**",
|
||||
"**/browser/**",
|
||||
"**/ghidra/**",
|
||||
"**/hopper/**",
|
||||
"**/replay/**",
|
||||
"**/cli.*",
|
||||
"**/main.*"
|
||||
],
|
||||
"message": "Domain tests may import only their owning domain and inward dependencies."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/application/**/*.test.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["**/server/**", "**/cli.*", "**/main.*"],
|
||||
"message": "Service tests must use application ports rather than CLI or MCP entrypoints."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/composition/**/*.test.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"**/server/**",
|
||||
"**/cli.*",
|
||||
"**/main.*",
|
||||
"node:child_process",
|
||||
"node:net",
|
||||
"node:http",
|
||||
"node:https",
|
||||
"ws",
|
||||
"playwright"
|
||||
],
|
||||
"message": "Composition tests may wire provider-neutral sessions but must not invoke CLI, MCP, process, network, or browser boundaries."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.5.1"
|
||||
".": "3.2.1"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Product Direction
|
||||
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; it supplies admitted read-only inventory and function-analysis operations but no GUI or mutation authority. Keep provider-specific code out of the domain and application layers.
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; it supplies admitted read-only inventory and function-analysis operations but no GUI or mutation authority. Keep provider-specific code out of the domain and application layers.
|
||||
|
||||
Prioritize:
|
||||
|
||||
@@ -17,121 +17,60 @@ REA is a local-only tool; do not sanitize actionable local diagnostics such as a
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
REA is a layered ESM TypeScript application. Dependencies flow inward: `domain` underlies `contracts` and the shared `process` primitives; providers depend on those layers, followed by `application`, `server`, and the entry adapters.
|
||||
REA is a layered ESM TypeScript application. Dependencies flow inward from pure domain logic through contracts, providers, application workflows, and CLI/MCP adapters. See [docs/architecture.mermaid](docs/architecture.mermaid) for the component map.
|
||||
|
||||
See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architecture diagram.
|
||||
|
||||
- `scripts/rea.mjs`: executable dispatcher. Routes only bare `mcp` and `--mcp` to the production stdio server; Incur handles registration utilities and one-shot commands.
|
||||
- `src/main.ts`: MCP adapter. Parses config, wires the shared session runtime, starts stdio transport, and owns process-lifetime shutdown.
|
||||
- `src/cli.ts`: one-shot CLI adapter for setup, diagnostics, analysis, and decompilation.
|
||||
- `src/config.ts`: Zod-validated parsing of environment configuration into `AppConfig`.
|
||||
- `src/domain/`: pure, side-effect-free modules. `errors.ts` owns the tagged error algebra; `result.ts` owns `Result`/`ok`/`err`; `hopperValues.ts` owns shared function-dossier values plus Hopper boundary parsers; `symbolAnalysis.ts` parses Swift/ObjC names; `javascriptApplicationGraph.ts` validates and canonically commits the provider-neutral JavaScript Application Graph; `javascriptStaticAnalysis.ts` performs bounded AST-only JavaScript structure recovery.
|
||||
- `src/contracts/`: caller-visible tool schemas and catalog metadata; `toolContracts.ts` owns the canonical inventory and `enhancedInputs.ts` owns enhanced input parsing.
|
||||
- `src/process/`: provider-neutral process ownership and lifecycle primitives. It owns private runtime roots, session-assigned run identity and token-verified lineage, absolute startup deadlines, correlated request waits, bounded output capture, and TERM-to-KILL cleanup without defining any provider wire protocol.
|
||||
- `src/replay/`: Linux x64 controlled-JavaScript-replay adapter. It owns exact runtime closure inspection, Bubblewrap/seccomp/cgroup admission, the disposable worker, strict parent/worker protocol validation, and complete cleanup observation.
|
||||
- `src/browser/`: loopback CDP/Inspector discovery, bounded WebSocket transport, exact-origin or canonical-root target authorization, passive browser/Electron and attach-only Node/Electron V8 observation, plus controlled Playwright scenarios with explicit launch/attach ownership and cleanup.
|
||||
- `src/hopper/`: Hopper launch and Unix-socket protocol mechanics. `BridgeLauncher.ts` spawns the Hopper app with the in-process bridge, `HopperClient.ts` correlates request/response over the socket with timeouts and cancellation, `protocol.ts` frames bridge messages.
|
||||
- `bridge/hopper_bridge.py`: runs inside Hopper and adapts declared operations to Hopper's public Python API. Hopper's bundled MCP server is not used.
|
||||
- `src/ghidra/`: exact Ghidra 12.1.2/JDK 21 inspection, analysis-profile commitment, digest-bound target snapshots, isolated `analyzeHeadless` launch, authenticated Unix-socket or Windows loopback transport, bounded serial request queue, and strict inventory/function boundaries.
|
||||
- `src/dotnet/`: execution-free managed PE/CLI inspection. It owns bounded PE, CLI metadata, heap, table, and resource parsing for `rea-dotnet-static`; it must never load, reflect, execute, decompile, or resolve target assemblies.
|
||||
- `bridge/ghidra/ReaGhidraBridge.java`: packaged read-only `HeadlessScript` loaded through Ghidra's `scriptPath`; it owns the persistent decompiler and adapts admitted inventory, function, reference, and CFG operations to public Ghidra APIs.
|
||||
- `src/application/`: shared CLI/MCP session composition, setup and diagnostics, and enhanced workflows. `AnalysisProviderRegistry` discovers overlapping deep candidates without starting them; `SessionProviderRouter` binds one candidate per target and composes it with disjoint auxiliary providers; `JavaScriptArtifactReconstruction.ts` safely projects local directories/ASARs and inert bundle syntax into the application graph without adding a caller-visible tool yet.
|
||||
- `src/server/`: MCP request translation. `createServer.ts` assembles the MCP server, `registerOfficialTools.ts`/`registerEnhancedTools.ts` register each tool set, `toolResult.ts` maps `Result` values to MCP content.
|
||||
- `docs/product-catalog.json`: generated package, tool-family, provider, setup-client, schema-version, and CLI facts. Regenerate it from source; do not edit it by hand.
|
||||
- `tests/`: Vitest suite. `tests/fixtures/` holds reusable provider-process fixtures, source-owned synthetic JavaScript graph and Electron/Webpack/Rspack artifact trees, and the fake launcher, Hopper bridge, and CDP seams.
|
||||
- `scripts/verify-real-hopper.mjs`: real-Hopper end-to-end verifier.
|
||||
- `scripts/verify-real-ghidra.mjs`: real Linux Ghidra verifier for x86-64 debug/stripped ELF, AArch64 ELF, PE, and Mach-O function semantics plus complete cleanup.
|
||||
- `scripts/verify-real-ghidra-windows.mjs`: real Windows x64 Ghidra P0 verifier for the source-owned native PE fixture, all 19 admitted operations, digest linkage, loopback transport, and cleanup.
|
||||
- `scripts/verify-real-browser.mjs`: real Chrome end-to-end verifier for the passive CDP provider.
|
||||
- `scripts/print-mcp-config.mjs`: prints an MCP server config with absolute paths filled in (`npm run config:print -- /path/to/binary`).
|
||||
- `src/domain/` owns pure provider-neutral semantics; `src/contracts/` owns caller-visible schemas and the canonical tool inventory.
|
||||
- `src/hopper/`, `src/ghidra/`, `src/browser/`, `src/replay/`, `src/native/`, `src/artifacts/`, and `src/dotnet/` own provider-specific boundaries. Keep provider protocols out of domain and application code.
|
||||
- `src/application/` composes shared CLI/MCP workflows; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points.
|
||||
- `src/process/` owns shared process lifecycle primitives, not provider wire protocols. `bridge/` contains provider-side adapters.
|
||||
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` contains real-toolchain checks.
|
||||
- `docs/product-catalog.json` is generated. Update its source contracts and regenerate it; do not edit it directly.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- `npm ci`: install exact lockfile dependencies.
|
||||
- `npm run deps:check`: verify that installed direct dependency versions match the root lockfile; on failure, run `npm ci`, then retry.
|
||||
- `npm run metadata:generate`: refresh checked-in package metadata after package, lockfile, or skill-version changes.
|
||||
- `npm run metadata:check`: verify checked-in package metadata without rewriting it.
|
||||
- `npm run build`: compile `src/` into `dist/`.
|
||||
- `npm run build:cached`: compile through Turbo and reuse a valid local build cache entry.
|
||||
- `npm test`: restore or build `dist/`, then run the Vitest suite once without coverage or retries locally.
|
||||
- `npm run test:fast`: run pure and subprocess test projects without the serial integration group.
|
||||
- `npm run test:changed`: run changed and related non-serial tests once.
|
||||
- `npm run test:integration`: run the serial filesystem, process, and CLI integration group.
|
||||
- `npm run test:watch`: watch changed and related non-serial tests, without coverage.
|
||||
- `npm run test:coverage`: run the complete suite with coverage; CI shards the suite and owns JUnit reporting.
|
||||
- `npm run typecheck`: run strict TypeScript checks without emitting files.
|
||||
- `npm run lint`: apply oxlint rules (complexity, max-lines, unused vars, and TypeScript-specific checks).
|
||||
- `npm run lint:fix`: auto-fix oxlint violations where possible.
|
||||
- `npm run format:check`: verify Prettier formatting.
|
||||
- `npm run check`: run cached typecheck, lint, format:check, knip, and package-metadata freshness tasks.
|
||||
- `npm run check:fast`: run only cached typecheck and lint for rapid local and pre-push feedback.
|
||||
- `npm run check:ci`: run the static gate plus duplicate-code and debt-marker scans.
|
||||
- `npm run check:test`: run the cached static tasks plus the uncached complete test suite.
|
||||
- `npm run check:pr`: run the test gate plus uncached generated-document validation for PR preparation.
|
||||
- `npm run knip`: detect unused files, dependencies, and exports.
|
||||
- `npm run jscpd`: detect duplicate code blocks.
|
||||
- `npm run scan:todos`: scan for TODO, FIXME, and HACK markers.
|
||||
- `npm run verify:hopper`: build and run the real-Hopper verifier with two distinct binaries.
|
||||
- `npm run verify:ghidra`: build and run the real-Ghidra verifier against `GHIDRA_INSTALL_DIR` and optional `GHIDRA_TARGET_PATH`.
|
||||
- `npm run verify:ghidra:windows`: build and run the real Windows Ghidra verifier against `GHIDRA_INSTALL_DIR` and the source-owned x64 PE fixture.
|
||||
- `npm run verify:browser`: build and run the real Chrome verifier against `REA_BROWSER_EXECUTABLE` or a platform-default Chrome-family executable, including browser-scenario CLI parity, attach disconnect-only ownership, launched-process termination, and temporary-profile cleanup.
|
||||
- `npm run verify:inspector`: build and run the real attach-only Node Inspector verifier through the public CLI.
|
||||
- `npm run verify:managed`: build and run the source-owned managed PE/CLI conformance verifier for artifact triage, member inspection, managed/native boundary declarations, token drift comparison, malformed metadata, non-managed degradation, managed application-graph projection, manifest-verifier self-test, and optional BYO ILSpy oracle checks; set `REA_MANAGED_APP_MANIFEST_PATH` to verify an operator-local managed app manifest, including optional graph/trace assertions, and set `REA_ILSPY_CMD_PATH` to an absolute `ilspycmd` path to run the real ILSpy reconstruction oracle.
|
||||
- `npm run evidence:generate`: regenerate the managed conformance manifest and Evidence v2 completion ledger from live verifier output.
|
||||
- `npm run evidence:check`: rerun managed conformance and fail on generated manifest, completion ledger, or bundled skill drift.
|
||||
- `npm run verify:replay`: build and run the real Linux Bubblewrap/seccomp/cgroup verifier against source-owned replay fixtures; set `REA_REPLAY_INPUT_PATH` to verify an operator-local manifest.
|
||||
- `npm run verify:package`: pack and test the CLI, setup transaction, skill, and canonical target-free MCP catalog in an isolated environment.
|
||||
- `npm run docs:api`: render uncommitted API documentation from JSDoc comments into `build/api-docs/` using TypeDoc.
|
||||
- `npm run docs:api:cached`: render API documentation through Turbo or restore a valid local result.
|
||||
- `npm run docs:generate`: refresh committed metadata and render the uncommitted API documentation.
|
||||
- `npm run docs:check`: verify generated package metadata, the canonical product catalog, caller-visible documentation facts, TypeDoc rendering, and the error JSON schema.
|
||||
- `npm run config:print -- /path/to/binary`: print an MCP server config with absolute paths.
|
||||
- `HOPPER_TARGET_PATH=/path/to/binary npm start`: launch Hopper and run the built stdio MCP server.
|
||||
- `npm ci`: install the locked dependencies.
|
||||
- `npm run build:cached`: build the CLI and MCP server.
|
||||
- `npm run test:local`: run focused tests for changed code.
|
||||
- `npm run check:fast`: run cached typecheck and lint checks.
|
||||
- `npm run check:pr`: run the deterministic PR gate and generated-document checks.
|
||||
- For provider-dependent changes, see [docs/testing.md](docs/testing.md) and run the matching real-provider verification.
|
||||
- Keep each verification lane's prerequisites limited to the claim it checks. Use host-native fixtures for host/provider acceptance; put optional cross-target formats and their external toolchains in a separate lane. Preflight required commands and report the missing dependency and lane clearly.
|
||||
|
||||
Pre-commit hooks via Husky format then lint staged source files. Pre-push runs `npm run check:fast`; CI owns the broader static and generated-document gates. Use `npm run lint:fix` to auto-correct violations locally.
|
||||
See [docs/testing.md](docs/testing.md) for test scopes and verification lanes and [CONTRIBUTING.md](CONTRIBUTING.md) for contribution checks. Pre-commit formats and lints staged source; pre-push runs `npm run check:fast`.
|
||||
|
||||
## Configuration & Environment Variables
|
||||
|
||||
- `REA_ANALYSIS_PROVIDER` (optional, default `auto`): require one deep-analysis provider ID for startup and one-shot commands, or use deterministic automatic selection. A request-level `provider_id`/`--provider` takes precedence.
|
||||
- `GHIDRA_INSTALL_DIR` (optional): absolute root of an extracted official Ghidra 12.1.2 distribution. The adapter supports Linux x64 and the experimental Windows x64 P0 boundary documented in `docs/windows-ghidra-p0.md`.
|
||||
- `JAVA_HOME` (optional): absolute 64-bit full JDK 21 root used by Ghidra. When absent, doctor probes `java`/`javac` from `PATH`.
|
||||
- `REA_ILSPY_CMD_PATH` (optional): absolute path to a bring-your-own `ilspycmd`; doctor reports its configured version, and `verify:managed` can use it as a real reconstruction oracle without making ILSpy a setup-installed dependency or canonical parser.
|
||||
- `HOPPER_TARGET_PATH` (optional): absolute initial binary or `.hop` target. Target-free MCP sessions use `open_binary` instead.
|
||||
- `HOPPER_LAUNCHER_PATH` (optional): override the Hopper executable path (defaults to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper`).
|
||||
- `HOPPER_TARGET_KIND` (optional, default `executable`): startup kind for `HOPPER_TARGET_PATH`; dynamic targets are classified from their paths and headers.
|
||||
- `HOPPER_LOADER_ARGS_JSON` (optional): JSON array overriding derived Hopper loader arguments for supported executable targets, e.g. `["-l","Mach-O","--aarch64"]`.
|
||||
- `REA_BROWSER_OBSERVE_ENABLED` (optional, default `false`): add browser observation authority to the administrator ceiling.
|
||||
- `REA_BROWSER_CDP_ENDPOINTS_JSON` (optional): approved literal loopback CDP HTTP endpoints.
|
||||
- `REA_BROWSER_ALLOWED_ORIGINS_JSON` (optional): exact HTTP(S) page origins approved for passive observation.
|
||||
- `REA_BROWSER_SCENARIO_ENABLED` (optional, default `false`): add controlled browser automation to the administrator ceiling.
|
||||
- `REA_BROWSER_SCENARIO_AUTO_GRANT` (optional, default `false`): issue the exact configured browser-automation ceiling as an administrator grant for trusted unattended use.
|
||||
- `REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON` (optional): canonical executable roots approved for provider-owned browser launch.
|
||||
- `REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON` (optional): literal-loopback CDP endpoints approved for external-browser attachment.
|
||||
- `REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON` (optional): exact HTTP(S) page origins approved for browser scenarios.
|
||||
- `REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON` (optional): environment-variable names that scenario secret references may resolve.
|
||||
- `REA_ELECTRON_OBSERVE_ENABLED` (optional, default `false`): add passive Electron file-page observation authority to the administrator ceiling.
|
||||
- `REA_ELECTRON_CDP_ENDPOINTS_JSON` (optional): approved literal loopback Electron CDP HTTP endpoints.
|
||||
- `REA_ELECTRON_FILE_ROOTS_JSON` (optional): canonical filesystem roots approved for passive Electron page observation.
|
||||
- `REA_V8_INSPECTOR_OBSERVE_ENABLED` (optional, default `false`): add passive attach-only Node/Electron Inspector observation authority to the administrator ceiling.
|
||||
- `REA_V8_INSPECTOR_ENDPOINTS_JSON` (optional): approved literal-loopback Node/Electron Inspector HTTP endpoints.
|
||||
- `REA_V8_INSPECTOR_FILE_ROOTS_JSON` (optional): canonical filesystem roots approved for Inspector targets and script locations.
|
||||
- `REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON` (optional): exact HTTP(S) origins approved for Electron renderer Inspector targets and scripts.
|
||||
- `REA_INVESTIGATION_INPUT_ROOTS_JSON` (optional): canonical filesystem roots approved for static JavaScript/Electron application analysis and other investigation inputs.
|
||||
- `REA_JAVASCRIPT_REPLAY_ENABLED` (optional, default `false`): add controlled extracted-module execution authority to the administrator ceiling.
|
||||
- `REA_JAVASCRIPT_REPLAY_ROOTS_JSON` (optional): exact canonical source roots approved for controlled replay.
|
||||
- `REA_JAVASCRIPT_REPLAY_NODE_PATH`, `REA_JAVASCRIPT_REPLAY_BWRAP_PATH`, `REA_JAVASCRIPT_REPLAY_SYSTEMD_RUN_PATH`, `REA_JAVASCRIPT_REPLAY_SYSTEMCTL_PATH`, and `REA_JAVASCRIPT_REPLAY_SHELL_PATH` (optional): absolute paths committed by each replay plan; defaults target the current Node runtime and standard Linux system executables.
|
||||
Configuration is parsed and validated by `src/config.ts` and `src/config/`. Keep user-facing setup and provider configuration in [README.md](README.md) and the relevant guide under `docs/`; do not maintain a second environment-variable catalogue here.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Use ESM TypeScript, two-space indentation, and Prettier defaults. Keep compiler strictness intact (`strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`). Use `camelCase` for values/functions, `PascalCase` for classes/types, and `UPPER_SNAKE_CASE` for constants. Parse unknown values at every MCP, environment, and subprocess boundary. Avoid `any`, unchecked casts, non-null assertions, import-time I/O, and floating promises. Exported APIs require concise JSDoc. Model expected failures with the tagged error algebra and `Result`, not broad exception wrappers.
|
||||
|
||||
## Designing MCP Tools
|
||||
|
||||
Start from the analyst question and desired result, not a provider API. Before adding a tool, inspect the existing contract and its nearest alternative.
|
||||
|
||||
- Prefer reusable, composable primitives: inspect one explicit object or
|
||||
relationship and return evidence that can support different analyst
|
||||
questions. Compose primitives into a workflow only for a recurring outcome
|
||||
that benefits from joining evidence sources; keep one-application or
|
||||
business-domain interpretations out of general tool contracts.
|
||||
- Use **inspect/search** tools for facts about a target or candidate set; use **trace** tools for relationships; use **compare** tools for explicitly paired inputs.
|
||||
- Add a **workflow** when observed agent use shows a repeated sequence that REA can compose without losing analyst control. Return useful results inline so callers can choose their next action.
|
||||
- Use **observe/capture/replay** only when runtime activity is required, and declare authority and lifecycle effects in the contract.
|
||||
- Extend an existing tool when intent and result contract are unchanged. Add a tool for a distinct analyst outcome or materially different authority.
|
||||
- Keep caller-facing names and results provider-neutral. Put engine-specific behavior in provider adapters and report each provider's exact coverage.
|
||||
- Keep results complete by default. Add a limit only when it follows from a real format, protocol, authority, or resource-safety constraint; explain truncation and unsupported facets. Keep observed, derived, inferred, and unknown results distinct.
|
||||
- Include artifact identity, source locations, Evidence references, actionable errors, and relevant limitations when they affect conclusions. Return the evidence needed for the next analysis inline rather than requiring a resource or identifier lookup.
|
||||
- Implement shared application workflows behind CLI and MCP adapters. Update canonical contracts and generated catalog together.
|
||||
|
||||
See [docs/tool-design.md](docs/tool-design.md) for the design checklist. When usability or tool selection changes, evaluate representative CLI/MCP tasks as well as schema and transport behavior.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, timeouts, process exit, concurrency, limits, and clean shutdown. Real Hopper, Ghidra, browser, JavaScript replay, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
|
||||
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, lifecycle cleanup, and actual format, protocol, and permission boundaries. Do not add tests that merely freeze arbitrary caps or prescribed call sequences. Real Hopper, Ghidra, browser, JavaScript replay, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
|
||||
|
||||
MCP tools are agent-computer interfaces. Optimize them first for successful agent use: distinct purposes, clear names and parameters, relevant result context, actionable errors, and realistic multi-step workflows. Validate tool-design changes with representative agent tasks and transcripts when possible; schema validity and transport success alone do not prove that a tool is easy for an agent to use.
|
||||
|
||||
MCP tool catalogs must remain complete and self-describing. Prefer capability- and session-scoped tool advertisement over schema truncation. Never use serialized byte counts as a proxy for model-context cost, token use, tool usability, or agent performance. Measure bytes only when evaluating a named transport, storage, or protocol constraint. Any model-context or token-efficiency claim must identify the client and its actual model-facing projection and tokenizer, since clients may omit, transform, dereference, or otherwise reproject MCP schemas. Do not weaken names, descriptions, schemas, examples, errors, or result context to improve a byte-based metric.
|
||||
Keep tool catalogs complete and self-describing; prefer capability- and session-scoped availability over schema truncation. Serialized bytes alone do not measure agent usability or model context cost.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
|
||||
+284
@@ -1,5 +1,289 @@
|
||||
# Changelog
|
||||
|
||||
## [3.2.1](https://github.com/morluto/rea/compare/rea-agents-3.2.0...rea-agents-3.2.1) (2026-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **release:** tolerate npm registry propagation delay ([726b730](https://github.com/morluto/rea/commit/726b730b84830b208725df50fa0251ece1fa199f))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* **release:** accept guarded npm publishing ([b7bc855](https://github.com/morluto/rea/commit/b7bc8554928c8eca9c29842c77e4021c1e42e054))
|
||||
|
||||
## [3.2.0](https://github.com/morluto/rea/compare/rea-agents-3.1.0...rea-agents-3.2.0) (2026-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **native:** add investigation features ([#486](https://github.com/morluto/rea/issues/486)) ([ed933d1](https://github.com/morluto/rea/commit/ed933d18933814e3957a920972ccf91117d4ed87))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **artifacts:** stabilize extraction approval ([7eb1a95](https://github.com/morluto/rea/commit/7eb1a9500c62b66716878c18ee442c324a715a88))
|
||||
* **browser:** reject unknown Electron input fields ([1860d4f](https://github.com/morluto/rea/commit/1860d4f0e531919ee9b10bec7ad8db43e18a291b))
|
||||
* **ci:** retain artifacts for delayed reruns ([07888ec](https://github.com/morluto/rea/commit/07888ec096f69753a1535703fb7b3c3070e786b7))
|
||||
* **cli:** remove ignored result limits ([f3645aa](https://github.com/morluto/rea/commit/f3645aa51e55c5207f7b9b1f0156ffaef7aaed42))
|
||||
* consolidate boundary ownership and cleanup failure paths ([#477](https://github.com/morluto/rea/issues/477)) ([2160d4f](https://github.com/morluto/rea/commit/2160d4fae581243990e539a9c401d1b8824e1763))
|
||||
* **contracts:** align byte read schema with provider ([f580d9d](https://github.com/morluto/rea/commit/f580d9d07382bc9642bce896d431c827955339e4))
|
||||
* **ghidra:** return complete native analysis results ([a66e43a](https://github.com/morluto/rea/commit/a66e43a99dda1604c8250c9eb73aa8ef000371c9))
|
||||
* **managed:** retain every source location ([33b2099](https://github.com/morluto/rea/commit/33b20991c455306d66f8687a9f80ea22073d4207))
|
||||
* **process:** capture complete host probe output ([6c68ff5](https://github.com/morluto/rea/commit/6c68ff565fe1196fd8c7a63598e8bd94098abc3c))
|
||||
* **runtime:** make observed target role optional ([593a8e1](https://github.com/morluto/rea/commit/593a8e12ceac1cfe350493cfe6b0d1360eb85f7c))
|
||||
* **runtime:** retain complete reconciled graph content ([bedd55a](https://github.com/morluto/rea/commit/bedd55a4b4ccc9a0c1441d3836138b323de6d126))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **analysis:** remove caller-selected result caps ([a8c58c6](https://github.com/morluto/rea/commit/a8c58c6605ac37a8b504167732affc5ce4fc772f))
|
||||
* **analysis:** remove fixed result quotas ([20152a9](https://github.com/morluto/rea/commit/20152a9872dd035714beb5ef5b8898a9b4d1d6c4))
|
||||
* **artifacts:** consolidate inventory into inspection ([0fe96d9](https://github.com/morluto/rea/commit/0fe96d92dfefbaf28d2abfd5e237f6d0e82eee7b))
|
||||
* **artifact:** simplify extraction and inventory results ([07ff8b3](https://github.com/morluto/rea/commit/07ff8b30ee497d9f4477a0a9ae97f3f5addbb45a))
|
||||
* **artifacts:** remove duplicate inventory wrapper ([aa6eee1](https://github.com/morluto/rea/commit/aa6eee1a712d1a319bbaed5387aa5f4429755a1f))
|
||||
* **artifacts:** remove selectors and traversal quotas ([8c24272](https://github.com/morluto/rea/commit/8c24272ee7e6d30b1493ee7ccd63e45734fe2f95))
|
||||
* **artifacts:** select extraction by logical path ([7ba3580](https://github.com/morluto/rea/commit/7ba3580c860463adcd5edc73b7a80f34b92480cf))
|
||||
* **browser:** remove arbitrary capture ceilings ([5dd3b46](https://github.com/morluto/rea/commit/5dd3b4655e21222a6598bf8cbc0be90f3cbf5e0f))
|
||||
* **browser:** return complete observation evidence ([0e2738f](https://github.com/morluto/rea/commit/0e2738ffb6aa35bb06fe01d0af864aec968570cf))
|
||||
* **browser:** return complete scenario observations ([e80fe41](https://github.com/morluto/rea/commit/e80fe414b13294446f959b382cb240c806cfe000))
|
||||
* **browser:** share authorized main-frame polling ([eec75ce](https://github.com/morluto/rea/commit/eec75ce918cbac576fd217847911793331aa32a0))
|
||||
* **browser:** share canonical identity digest ([a6d9e49](https://github.com/morluto/rea/commit/a6d9e49ab46150c8b757f5bdd5f80d3a8ff52f26))
|
||||
* **contracts:** remove duplicate schema aliases ([4593c29](https://github.com/morluto/rea/commit/4593c29b64d3f6c43ddb19228da6231f1055eba4))
|
||||
* **contracts:** simplify workflows and drop readiness tool ([7acac61](https://github.com/morluto/rea/commit/7acac615ca2b8e3f4edd65fd8a08b237f85b5ac7))
|
||||
* **coverage:** evaluate inline without workspaces ([9e76054](https://github.com/morluto/rea/commit/9e7605482acf2de77ad04c5c5e03ac4280a11d7f))
|
||||
* **domain:** encode comparison and lifecycle states exactly ([#473](https://github.com/morluto/rea/issues/473)) ([d65730c](https://github.com/morluto/rea/commit/d65730c4c24ab6183426516d4564e6c25294b35d))
|
||||
* **domain:** share AST property name reader ([4f4cc59](https://github.com/morluto/rea/commit/4f4cc59873ec704811649aeca8a49e3bcdbdf324))
|
||||
* **electron:** retain complete active observations ([e8c56e0](https://github.com/morluto/rea/commit/e8c56e0c0aeb114e09636ad1841d8fd73720b5cc))
|
||||
* **errors:** remove stale workspace failures ([f570d40](https://github.com/morluto/rea/commit/f570d40f50bc8621b32ba4e255660ed1b1398356))
|
||||
* **evidence:** remove obsolete ID resolvers ([a3920e2](https://github.com/morluto/rea/commit/a3920e2abd915eba31b59413710315a85caf1afa))
|
||||
* **evidence:** remove session ledger quotas ([dca293a](https://github.com/morluto/rea/commit/dca293a446fa86767fb4bed6fe734f09b6cfafde))
|
||||
* **files:** use caller paths for local evidence files ([c34efca](https://github.com/morluto/rea/commit/c34efca4cd322d4816ab4cd526a7475489edc814))
|
||||
* **ghidra:** remove redundant schema markers ([c2a6f4b](https://github.com/morluto/rea/commit/c2a6f4bc437671c25ba318d21b82818c8e6bf2ce))
|
||||
* **javascript:** remove analysis ceilings ([9aec690](https://github.com/morluto/rea/commit/9aec6908b5c7ad50da6882415700cbe53c3f62ed))
|
||||
* **managed:** derive inspection from PE CLI extents ([dfad09e](https://github.com/morluto/rea/commit/dfad09e3b5769b079bf2d07382aee310d682724f))
|
||||
* **mcp:** simplify evidence and workflows ([6740ed5](https://github.com/morluto/rea/commit/6740ed5215578eaaf33c785d6245649d76a0411c))
|
||||
* **mcp:** simplify tool inputs and inline results ([f55c2b9](https://github.com/morluto/rea/commit/f55c2b92676e4a7eb06efbc8d9a908a7de62541a))
|
||||
* **mcp:** simplify workflow and observation scope ([34d00fd](https://github.com/morluto/rea/commit/34d00fd323c793278c6ed126308c58e2fc7efd54))
|
||||
* **observation:** internalize capture budgets ([7b3c797](https://github.com/morluto/rea/commit/7b3c7972377c7f77b1328e7d1de3f54835bc1397))
|
||||
* **observation:** retain complete tool results ([fa2b6c5](https://github.com/morluto/rea/commit/fa2b6c5a61adcb75c4c2773ed1c9f7dba83fc1d3))
|
||||
* **observation:** return complete inventories ([c523c38](https://github.com/morluto/rea/commit/c523c38027f536e8319eb2acc1840274c5bc5986))
|
||||
* **permission:** remove unused authorization helpers ([1b0ab75](https://github.com/morluto/rea/commit/1b0ab75a84ccdcf036ccfc019d1890b2d626699a))
|
||||
* **process:** remove fixed scenario ceilings ([abf7c11](https://github.com/morluto/rea/commit/abf7c1142e759a291d2267fedf15b0dd444a5684))
|
||||
* **process:** remove redundant schema version markers ([b3aca52](https://github.com/morluto/rea/commit/b3aca5289bf973ffb4072253eebe86e0acced347))
|
||||
* **process:** remove request count ceilings ([f2dc749](https://github.com/morluto/rea/commit/f2dc749c862a16d056de53e95914a9a411592583))
|
||||
* **process:** remove unused paired experiment ([e1d755f](https://github.com/morluto/rea/commit/e1d755f7cd574860f59965a6244dbd43f5032f2c))
|
||||
* **prompts:** make tool workflows optional ([3fd9650](https://github.com/morluto/rea/commit/3fd96506f223889a2d327cc7c97a95bf48ccb594))
|
||||
* **reference:** share graph index projections ([6ee87fb](https://github.com/morluto/rea/commit/6ee87fbf2defcb10a17352b36ebeed67c6ce6dc5))
|
||||
* remove cross-version investigation workflow ([81996df](https://github.com/morluto/rea/commit/81996df7a59ae6efb0f7be6687af89e66c64cdcb))
|
||||
* **replay:** share sandbox probe arguments ([9fca6c0](https://github.com/morluto/rea/commit/9fca6c0296ac360f9d2c838440b24d2ea79234b5))
|
||||
* **replay:** simplify runtime value checks ([60cb68a](https://github.com/morluto/rea/commit/60cb68a276b012964b08c18e9d60af22e64c468f))
|
||||
* return complete analysis results inline ([a2a3243](https://github.com/morluto/rea/commit/a2a3243ea874ee1662c62a83ced63be218e0218e))
|
||||
* return complete tool results inline ([0d96275](https://github.com/morluto/rea/commit/0d96275b6887b72519d2bc54f72012db6a8e482c))
|
||||
* **runtime:** retain complete reconciliation outputs ([79ea571](https://github.com/morluto/rea/commit/79ea571512a1f55200eb5520d6c56104dba54e5b))
|
||||
* **schema:** remove data-only version markers ([5457f4a](https://github.com/morluto/rea/commit/5457f4a2bceccb9c40e19a9986964025f279fc91))
|
||||
* **server:** remove registration alias ([8097ec4](https://github.com/morluto/rea/commit/8097ec456a14992b578057438be8a6e89a076cdf))
|
||||
* **server:** use canonical elicitation type ([bb6a58c](https://github.com/morluto/rea/commit/bb6a58c2a7eec15fb5f224376230398b51d1b952))
|
||||
* **test:** colocate pure-domain tests and reduce test debt ([#471](https://github.com/morluto/rea/issues/471)) ([e91c8c3](https://github.com/morluto/rea/commit/e91c8c39c2acddc4389ed0d01950d1954c472e47))
|
||||
* **test:** keep coverage at behavioral boundaries ([#474](https://github.com/morluto/rea/issues/474)) ([d4dc940](https://github.com/morluto/rea/commit/d4dc940bd871089ffa1b42d3475798b2f4f9c992))
|
||||
* **tools:** remove arbitrary workflow ceilings ([5f73cb0](https://github.com/morluto/rea/commit/5f73cb061b6fec532d1cccf5a8bce59855823fa4))
|
||||
* **tools:** remove artificial analysis ceilings ([2f08c7c](https://github.com/morluto/rea/commit/2f08c7c932d3619a481709935f07da59de3d3b27))
|
||||
* **tools:** remove redundant limits and acknowledgements ([bd0e341](https://github.com/morluto/rea/commit/bd0e34157239613aed7aa9315df0e97d03d8636c))
|
||||
* **tools:** remove remaining arbitrary workflow caps ([ecf3ed8](https://github.com/morluto/rea/commit/ecf3ed8bdb9ef7a83e7a2fb23443eced6c3824b0))
|
||||
* **tools:** return complete analysis results inline ([2a6eb41](https://github.com/morluto/rea/commit/2a6eb41147e496b9e044011fa3b755ef74388c51))
|
||||
* **workflows:** simplify evidence reference inputs ([24e1fa9](https://github.com/morluto/rea/commit/24e1fa9a76e447cff318da9f48c7adb659a1f8ab))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add REA tool design guidance ([#485](https://github.com/morluto/rea/issues/485)) ([36aeec9](https://github.com/morluto/rea/commit/36aeec95863ca28d3e3aa5bec75a45d3824e17f5))
|
||||
* clarify aggregate context tools ([c7e207c](https://github.com/morluto/rea/commit/c7e207c290e376db5c2f210f9ac08fe170ef3ec4))
|
||||
* clarify MCP tool design guidance ([36aeec9](https://github.com/morluto/rea/commit/36aeec95863ca28d3e3aa5bec75a45d3824e17f5))
|
||||
* correct inline evidence and Hopper behavior ([bf9cce1](https://github.com/morluto/rea/commit/bf9cce1aef89a769320d217e82b895b086880e69))
|
||||
* describe complete application analysis outputs ([271b162](https://github.com/morluto/rea/commit/271b1620e68f3a6bbc69f28aa4c4d18d5c453baf))
|
||||
* generalize reverse-engineering workflow ([#488](https://github.com/morluto/rea/issues/488)) ([4ecc7a7](https://github.com/morluto/rea/commit/4ecc7a72f2d6d78b8d9c59a146e532fe552c40bb))
|
||||
* improve GitHub issue and pull request templates ([c490d85](https://github.com/morluto/rea/commit/c490d85045de07488e01747db9c9f77d264b2e36))
|
||||
* prioritize reusable analysis primitives ([#493](https://github.com/morluto/rea/issues/493)) ([9dd72c4](https://github.com/morluto/rea/commit/9dd72c4911841671594570677f5ba6e3992d076d))
|
||||
* refresh managed conformance manifest ([23e56c5](https://github.com/morluto/rea/commit/23e56c517c08abcafe3a81919215433458aef27e))
|
||||
* refresh tool catalog and cleanup audit fixtures ([53db3a4](https://github.com/morluto/rea/commit/53db3a476b2881f498a5d6c87195e783268c1988))
|
||||
* regenerate tool and evidence catalogs ([2f604a3](https://github.com/morluto/rea/commit/2f604a3c3ad037a68da4c33a30cb5316983b070f))
|
||||
* regenerate tool and evidence catalogs ([f2bbd59](https://github.com/morluto/rea/commit/f2bbd5957eccac6164b3f12db87921ffb09bdcf2))
|
||||
* **tools:** clarify complete inline results ([3b64c81](https://github.com/morluto/rea/commit/3b64c813cf8bcd0cdd262d0a3d5950819f320a12))
|
||||
* **tools:** clarify complete inline workflows ([4168174](https://github.com/morluto/rea/commit/41681742ccea83a7956e7bc303da4aa223ed4877))
|
||||
* **tools:** describe uncapped tool behavior accurately ([4cea84d](https://github.com/morluto/rea/commit/4cea84d7cbb0508e6c0dde36a2a0542855681f95))
|
||||
* **tools:** update inline workflow guidance ([4ad937c](https://github.com/morluto/rea/commit/4ad937c6caeec89ded592ea82dec4fa13ea17d8c))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* **artifacts:** exercise argument-free extraction preflight ([a6d5b3e](https://github.com/morluto/rea/commit/a6d5b3ea9d1f53afeda33ef53fe50f42feac2343))
|
||||
* **browser:** assert strict inputs without legacy fields ([9ddf22e](https://github.com/morluto/rea/commit/9ddf22e1962803bf06d54b2e577a0c03a3f3ad48))
|
||||
* **browser:** cover complete screenshot output ([3f547ea](https://github.com/morluto/rea/commit/3f547eafa4c6f27b8264025c754392161687908e))
|
||||
* **browser:** remove stale input field assertion ([60c110d](https://github.com/morluto/rea/commit/60c110d97b1b22c010b8597965b3ad09f4f51480))
|
||||
* **cli:** consolidate duplicate setup journey ([9bd7e2c](https://github.com/morluto/rea/commit/9bd7e2cb459d0354ee2c406555de9e427c549086))
|
||||
* **cli:** remove duplicate browser discovery case ([dcb0362](https://github.com/morluto/rea/commit/dcb036292e975c81780dbf7a711fd1a565a13a10))
|
||||
* **conformance:** remove fixture generator test option ([ea853ce](https://github.com/morluto/rea/commit/ea853cebde5641dc9c12ef59005de84197463467))
|
||||
* **contracts:** remove duplicate tool inventories ([0ae3c08](https://github.com/morluto/rea/commit/0ae3c0837b387e53686bcd6e8aa03cb29f80458a))
|
||||
* **hopper:** name regex search assertion accurately ([9240bd8](https://github.com/morluto/rea/commit/9240bd8d166ffee93a90fd55c5de4b58d8ff6ad4))
|
||||
* **hopper:** remove dossier identity assertion ([b5c0b5b](https://github.com/morluto/rea/commit/b5c0b5b53e2d15dd6da9d4e8b1d3c3c21a16c409))
|
||||
* **mcp:** align workflow assertions with current guidance ([f8bb113](https://github.com/morluto/rea/commit/f8bb113c22cf23916a8cd8a5d77734ccaf63fd08))
|
||||
* **mcp:** consolidate catalog inventory checks ([3afcd64](https://github.com/morluto/rea/commit/3afcd641462bbd99ebdc3bf2fc8264e2c3344c09))
|
||||
* **mcp:** validate defaults at tool boundary ([21241d2](https://github.com/morluto/rea/commit/21241d2323d323f405b3cff4692e01364b6ddbde))
|
||||
* remove duplicate export schema acceptance ([b783e7f](https://github.com/morluto/rea/commit/b783e7febef8c22b41817a61a7f8293393ac6425))
|
||||
* remove obsolete schema version assertions ([fe342fa](https://github.com/morluto/rea/commit/fe342faa832ec9b17a6fb1dd85b1cf269b33cf1b))
|
||||
* remove stale limits and handoff expectations ([acf33d6](https://github.com/morluto/rea/commit/acf33d6df272873728aaa7527c5d43fa2a13132b))
|
||||
* remove vacuous and duplicate assertions ([56e2bf7](https://github.com/morluto/rea/commit/56e2bf7696e6ccbc3e4b5baf9249c6d4152f4a43))
|
||||
* remove Vitest config test exports ([d6e545c](https://github.com/morluto/rea/commit/d6e545cc4832ed47cc98d974aa8e4aa9453afb98))
|
||||
* replace brittle workflow and readme checks ([b3529aa](https://github.com/morluto/rea/commit/b3529aa327026472798a5f0971dcb325d221ddc6))
|
||||
* replace implementation lock-in with behavioural assertions ([#498](https://github.com/morluto/rea/issues/498)) ([7ee3f23](https://github.com/morluto/rea/commit/7ee3f236a9f753dfc90d84f95f20598e4e45fd0d))
|
||||
* **replay:** remove duplicate schema coverage ([4ec2343](https://github.com/morluto/rea/commit/4ec2343d91925e583b15d5404af2eabaf6ffed0f))
|
||||
|
||||
## [3.1.0](https://github.com/morluto/rea/compare/rea-agents-3.0.0...rea-agents-3.1.0) (2026-08-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **electron:** add autonomous Electron runtime analysis ([#464](https://github.com/morluto/rea/issues/464)) ([8f229ed](https://github.com/morluto/rea/commit/8f229ede70c0a4e224c1cf71d61487e17b779d6a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** guard Release Please PR branch parsing ([84cc819](https://github.com/morluto/rea/commit/84cc819284b9ce7ee648850c85d5daf3a33e5a24))
|
||||
* **ci:** guard release PR branch parsing ([6838b6b](https://github.com/morluto/rea/commit/6838b6bff9ab5c400432caefd8c8737b86c41f07))
|
||||
* harden evidence lifecycle and MCP contracts ([5155924](https://github.com/morluto/rea/commit/51559245d2e1c2d7550cfe489716950fe6154bd0))
|
||||
* harden structured configuration comparisons ([#467](https://github.com/morluto/rea/issues/467)) ([6901edc](https://github.com/morluto/rea/commit/6901edc05d56063bdbe8be96511b1b21863630b5))
|
||||
* **npm:** preserve explicit setup versions ([#465](https://github.com/morluto/rea/issues/465)) ([1783bf6](https://github.com/morluto/rea/commit/1783bf6dfdd4ec11e824870ce5b1a2f5a321450e))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **mcp:** make analysis states unrepresentable ([#468](https://github.com/morluto/rea/issues/468)) ([b8a9da0](https://github.com/morluto/rea/commit/b8a9da0c0f9f0b2b022a350d2c7c671d1fa3f886))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* clarify agent reverse-engineering promise ([ed4a628](https://github.com/morluto/rea/commit/ed4a62871f7924948e3774ae6cf880514a560ff4))
|
||||
* clarify the agent reverse-engineering promise ([#466](https://github.com/morluto/rea/issues/466)) ([ed4a628](https://github.com/morluto/rea/commit/ed4a62871f7924948e3774ae6cf880514a560ff4))
|
||||
* **npm:** simplify current setup command ([#469](https://github.com/morluto/rea/issues/469)) ([8b62b1a](https://github.com/morluto/rea/commit/8b62b1a71586e9409e4dd645f3d4947ef61e59b1))
|
||||
|
||||
## [3.0.0](https://github.com/morluto/rea/compare/rea-agents-2.7.0...rea-agents-3.0.0) (2026-08-01)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* export_evidence_bundle now requires a filesystem path and no longer returns an inline bundle.
|
||||
|
||||
### Features
|
||||
|
||||
* make evidence bundles canonical MCP resources ([1712e6d](https://github.com/morluto/rea/commit/1712e6d821965ce5edc3880fe4ebab2e3cc02fd4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* address MCP resource review feedback ([f3b3fb0](https://github.com/morluto/rea/commit/f3b3fb07ded3d1ad72c0a4f24ca3147ef9722c5d))
|
||||
* align aggregate tools with session capabilities ([ede91ff](https://github.com/morluto/rea/commit/ede91ff1f04563001f695ec3869daeb3c826154a))
|
||||
* **ci:** align Vitest project worker budgets ([f47ee1a](https://github.com/morluto/rea/commit/f47ee1abb4fc8dbd5da3fa13c5870d6580b28bfa))
|
||||
* **ci:** allow documented public type aliases ([97f6a0f](https://github.com/morluto/rea/commit/97f6a0f3fd2ebf8fa884d7f7ba6e1c66ecfe80e0))
|
||||
* **ci:** make coverage report merging composable ([788f405](https://github.com/morluto/rea/commit/788f4057aee65dfae441249d2c9789bb996912a7))
|
||||
* **ci:** stabilize Vitest project routing and shards ([f07acde](https://github.com/morluto/rea/commit/f07acde513d4c74d027b66e143cf5072246b412b))
|
||||
* **errors:** preserve actionable analysis diagnostics ([0f96bf3](https://github.com/morluto/rea/commit/0f96bf365c330a7eb121a173bfa140cc2b652764))
|
||||
* **mcp:** clarify routing and advertise input examples ([969519c](https://github.com/morluto/rea/commit/969519c00f31b5bc0f23dcffe8867e091461fda6))
|
||||
* **mcp:** clarify target routing and caller diagnostics ([3e1e8cd](https://github.com/morluto/rea/commit/3e1e8cd3f829e6d3ac8b3c63d979f9db84dec826))
|
||||
* **test:** bound boundary worker pressure ([935721d](https://github.com/morluto/rea/commit/935721df10d25d23621dbc2be90d334644e0858c))
|
||||
* **test:** honor boundary ownership and public aliases ([92a1c33](https://github.com/morluto/rea/commit/92a1c33625ff88d81581554687e7708dd3c91e2d))
|
||||
* **test:** retain subprocess coverage and cleanup ([7f46bb1](https://github.com/morluto/rea/commit/7f46bb18241bf3815e5e01816277d0cafe8f923b))
|
||||
* **test:** retain subprocess coverage and cleanup ([dae2b9c](https://github.com/morluto/rea/commit/dae2b9cc4e8753827702ada6e51f2ee01811cfed))
|
||||
* **test:** stabilize boundary timing ([f0d8cda](https://github.com/morluto/rea/commit/f0d8cdafa86bafdc6ad5d8c2d23a97ef583b53d5))
|
||||
* **verify:** follow dynamic Hopper tool availability ([281e41f](https://github.com/morluto/rea/commit/281e41f7660066addcc7a204128fb1f49bf7a674))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* preserve provider-neutral runtime seams ([a8161f0](https://github.com/morluto/rea/commit/a8161f0e5540f3261a08015dfe56a4254980c064))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* document resource snapshot workflow ([2a45d8d](https://github.com/morluto/rea/commit/2a45d8de7d52d0a234672d03c7dae794859f8ced))
|
||||
* refresh generated SDK metadata ([c824db1](https://github.com/morluto/rea/commit/c824db1ab7aed2886e9050c1b7fe2136b77e3698))
|
||||
* refresh managed completion manifest ([59d4659](https://github.com/morluto/rea/commit/59d465971ea86f933e9037fa32ac2787d935d076))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* **contracts:** cover validation branches ([8ccdb33](https://github.com/morluto/rea/commit/8ccdb33dd25922e039140bcd1e3cbf3d5acee837))
|
||||
* derive MCP SDK identity expectations ([ab4193c](https://github.com/morluto/rea/commit/ab4193c1f48020ff6baa42cb75e033110ee72b36))
|
||||
* **eval:** cover navigation and address tool routing ([ba098f2](https://github.com/morluto/rea/commit/ba098f24e59efbd567ae8438f65e31af0f0b8050))
|
||||
* overhaul deterministic suite architecture ([48053d1](https://github.com/morluto/rea/commit/48053d1032324cce6e2fe782ecba43d1b91a1bba))
|
||||
* overhaul deterministic suite architecture ([513d9b1](https://github.com/morluto/rea/commit/513d9b15558f9174a8e2c0ab00e31cbc05dbaba7))
|
||||
* **package:** verify npx upgrade on published releases ([7b6f7be](https://github.com/morluto/rea/commit/7b6f7becd257170cebf80f4f9f9c385b0000f074))
|
||||
* **package:** verify npx upgrade on published releases ([1f8ad7b](https://github.com/morluto/rea/commit/1f8ad7bc68f50d396715116467f19524da7e05e6))
|
||||
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
* document deterministic suite ownership ([3c42403](https://github.com/morluto/rea/commit/3c42403b78b53e7b5358c2bca9bc156ef2c32efd))
|
||||
|
||||
## [2.7.0](https://github.com/morluto/rea/compare/rea-agents-2.6.0...rea-agents-2.7.0) (2026-07-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **bytecode:** add JVM and Python bytecode providers ([1a75f8d](https://github.com/morluto/rea/commit/1a75f8dc236d8269a5fa07e042471ac79e79d728))
|
||||
* **bytecode:** add JVM and Python bytecode providers ([4e0127d](https://github.com/morluto/rea/commit/4e0127d620377e5dc7e09444f9db0977e3d47ed1)), closes [#367](https://github.com/morluto/rea/issues/367)
|
||||
* **conformance:** add portable conformance export package format ([91fb87d](https://github.com/morluto/rea/commit/91fb87d17e7885439762667c96167af79a2e044e))
|
||||
* **conformance:** add portable conformance export package format ([4ee30e4](https://github.com/morluto/rea/commit/4ee30e4145db9b496158cd980af05e22010856a7))
|
||||
* **metadata:** add deeper ObjC/Swift metadata and database save/licensing ([4602efd](https://github.com/morluto/rea/commit/4602efd27cbf8701c2dc15542042bc9d9ef76e7f))
|
||||
* **metadata:** add deeper ObjC/Swift metadata and database save/licensing ([d7a0753](https://github.com/morluto/rea/commit/d7a07535b0fb61ee8f9ae648bb673dda23290653)), closes [#353](https://github.com/morluto/rea/issues/353)
|
||||
* **mobile:** add Android APK/AAB/DEX and iOS IPA static investigation providers ([18624df](https://github.com/morluto/rea/commit/18624df5724a2c4d14a4e8e178660456424b2344))
|
||||
* **mobile:** add Android APK/AAB/DEX and iOS IPA static investigation providers ([fdeeb27](https://github.com/morluto/rea/commit/fdeeb27f23041c19b5857fe7873106f0cd9c6244)), closes [#366](https://github.com/morluto/rea/issues/366)
|
||||
* **package:** add MSIX/MSI/AppX package, resource, and signature analysis ([0ef1b91](https://github.com/morluto/rea/commit/0ef1b91cf31f6ca53991ee5ca60b4a22f430c01e))
|
||||
* **package:** add MSIX/MSI/AppX package, resource, and signature analysis ([6c03f83](https://github.com/morluto/rea/commit/6c03f83b1bd55095d136aeb3700733383365ecc4)), closes [#363](https://github.com/morluto/rea/issues/363)
|
||||
* **pe:** add broader PE/COFF/PDB static inspection ([f942558](https://github.com/morluto/rea/commit/f9425585736a89165390178f245b0f0f5d6cdacf))
|
||||
* **pe:** add broader PE/COFF/PDB static inspection ([94e6600](https://github.com/morluto/rea/commit/94e6600faf89436e4e2689fda8050792307b268d)), closes [#362](https://github.com/morluto/rea/issues/362)
|
||||
* **process:** replace procfs sampling with event-backed process-tree capture ([a5e355a](https://github.com/morluto/rea/commit/a5e355a3d52b19e89535449cd923c35e56ccc60d))
|
||||
* **process:** replace procfs sampling with event-backed process-tree capture ([d5d2e56](https://github.com/morluto/rea/commit/d5d2e56d0461cb0debba540bc658a9938ac30599)), closes [#332](https://github.com/morluto/rea/issues/332)
|
||||
* **protocol:** add custom TCP/UDP/IPC/XPC and auth-flow capture ([1fb22b4](https://github.com/morluto/rea/commit/1fb22b4c21e3a5581f3928003cc32ddc1cc9530b))
|
||||
* **protocol:** add custom TCP/UDP/IPC/XPC and auth-flow capture ([6fa6f9b](https://github.com/morluto/rea/commit/6fa6f9b6d9595a480a590766dadcfebb8e7111b5)), closes [#365](https://github.com/morluto/rea/issues/365)
|
||||
* **protocol:** add gRPC/Protobuf and JSON-RPC/MessagePack capture ([54153ed](https://github.com/morluto/rea/commit/54153ed62cb4f55d1efd4794490c2481ab34e212))
|
||||
* **protocol:** add gRPC/Protobuf and JSON-RPC/MessagePack capture ([3f336fe](https://github.com/morluto/rea/commit/3f336fefb132971bc6bb37b2b59aa01a5e3be5ca)), closes [#364](https://github.com/morluto/rea/issues/364)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **catalog:** digest provider projections ([645668b](https://github.com/morluto/rea/commit/645668b70dfdff74582d4ac26cda417947231764))
|
||||
* **catalog:** digest provider projections ([c4cd033](https://github.com/morluto/rea/commit/c4cd0330e00eeb8e272abbf6ecc6ce961e72f317))
|
||||
|
||||
## [2.6.0](https://github.com/morluto/rea/compare/rea-agents-2.5.1...rea-agents-2.6.0) (2026-07-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* complete evidence-backed roadmap workflows ([1a0931b](https://github.com/morluto/rea/commit/1a0931b630587701d495d1634fbafe3638ad5516))
|
||||
* **hopper:** report bridge progress diagnostics ([7a43420](https://github.com/morluto/rea/commit/7a434200297df85c62c02bb103cf870d14f202f7))
|
||||
* **native:** inspect API boundaries with evidence ([521139d](https://github.com/morluto/rea/commit/521139d4a57a8ab2e25bb262d2a1fd09fd06be92))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **setup:** refresh stale local npx bootstrap ([6d37efe](https://github.com/morluto/rea/commit/6d37efe8fa1fb9b09c8eaa3a1ed6b6f63188ae22))
|
||||
* **setup:** refresh stale local npx bootstrap ([90bb161](https://github.com/morluto/rea/commit/90bb1610f56448846adf85b31d9ae96be12dfb4a))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **mcp:** reduce cold-start work ([8ec9bb1](https://github.com/morluto/rea/commit/8ec9bb1bfea8bc57441bec72f3fcfbe93cf5d4f4))
|
||||
* **mcp:** reduce cold-start work ([fecddc0](https://github.com/morluto/rea/commit/fecddc0549a31e99a5ea72cbda84e448b1a961d5))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* add critical provider workflow coverage ([4716fa2](https://github.com/morluto/rea/commit/4716fa2ece954f5e53bdda9d1f33afad57d2e151))
|
||||
* **hopper:** remove brittle diagnostic source assertion ([40bf55b](https://github.com/morluto/rea/commit/40bf55b74ca6b2c90b3243a29018cb75879ba081))
|
||||
* **investigation:** prove multi-version fixture closure ([5184816](https://github.com/morluto/rea/commit/5184816199895ceff6a489810e12fba0c2c41eb7))
|
||||
* **mcp:** stabilize lazy-loading verification ([7157ab0](https://github.com/morluto/rea/commit/7157ab0b70833cc8237271e8625ccb7965c318c3))
|
||||
* **package:** prove artifact cleanup and identity ([9522adf](https://github.com/morluto/rea/commit/9522adf19ac915247e2c74b18d3ef5cab95befc5))
|
||||
|
||||
## [2.5.1](https://github.com/morluto/rea/compare/rea-agents-2.5.0...rea-agents-2.5.1) (2026-07-25)
|
||||
|
||||
|
||||
|
||||
+9
-9
@@ -2,6 +2,8 @@
|
||||
|
||||
REA welcomes focused bug fixes, documentation improvements, tests, and reverse-engineering workflow enhancements. Open an issue before a large contract or architecture change so its scope can be agreed before implementation.
|
||||
|
||||
When adding or changing an MCP tool, follow the [tool design guide](docs/tool-design.md) and preserve the canonical contracts and generated catalog.
|
||||
|
||||
## Development setup
|
||||
|
||||
REA development requires Node.js 24.18.x and npm 11.16.x. Real-Hopper verification additionally requires either macOS 12+ or an officially supported Linux host (Ubuntu 24.04+, Fedora 41+, or 64-bit Arch) and an installed Hopper application. Linux demo verification uses its own private Xvfb display and does not require a desktop session. Run `nvm use` before installing dependencies.
|
||||
@@ -38,15 +40,13 @@ provider and replay execution remains uncached, but their deterministic build
|
||||
prerequisite uses Turbo. CI uploads the rendered TypeDoc site as an `api-docs`
|
||||
artifact; the generated HTML is not committed.
|
||||
|
||||
Local `npm test` runs without coverage, retries, or verbose output. Use `npm run
|
||||
test:fast` for pure and subprocess groups, `npm run test:integration` for serial
|
||||
filesystem/process/CLI cases. `npm run test:changed` runs changed and related
|
||||
non-serial tests once; `npm run test:watch` keeps that selection live. Pure tests
|
||||
use Vitest threads, while subprocess tests stay in isolated forks. CI splits the
|
||||
complete suite across two native Vitest
|
||||
shards, then merges their coverage and JUnit reports. `npm run test:ci` runs the
|
||||
equivalent unsharded gate locally. Coverage thresholds remain in
|
||||
`vitest.config.ts`.
|
||||
Local `npm test` runs every deterministic Vitest project without coverage,
|
||||
retries, or verbose output. Focused boundary, acceptance, changed-test, and
|
||||
watch commands are documented in [docs/testing.md](docs/testing.md), together
|
||||
with the path-based behavioral-depth rules. Changed-test selection follows the
|
||||
import graph and is feedback, not the release gate. CI splits the complete suite
|
||||
across four native Vitest shards, then merges coverage, JUnit, and JSON timing
|
||||
reports. Coverage thresholds remain in `vitest.config.ts`.
|
||||
|
||||
CI installs dependencies once for all static checks, cancels superseded PR
|
||||
runs, and skips package, Windows, and full test lanes for documentation-only
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
### One CLI and MCP server for agents to reverse engineer anything
|
||||
### Reverse engineer anything with agents, from app behavior down to native binaries.
|
||||
|
||||
**See a feature you like. Understand how it works, down to the binary level.**
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
See a feature in an app that you want in your own product? Give the app to your agent—even without its source code. With REA, the agent can investigate the feature, explain how it works, show its evidence, and build a version adapted to your stack and requirements.
|
||||
|
||||
REA gives agents one consistent way to investigate software. Today that includes deep native analysis and function dossiers through Hopper or bring-your-own Ghidra on Linux, plus an experimental Windows x64 Ghidra P0 for approved native PE applications; execution-free managed PE/CLI triage; reproducible Evidence v2 records; controlled process capture; passive website, Electron page, and Node/Electron V8 Inspector observation; bounded JavaScript/source-map reconstruction; and a versioned domain graph for connecting JavaScript application layers without confusing static inference with runtime observation. The longer-term toolkit extends the same agent workflow to APIs, protocols, mobile artifacts, firmware, richer runtime behavior, and differences between versions.
|
||||
REA gives agents one consistent way to investigate software. Today that includes deep native analysis and function dossiers through Hopper or bring-your-own Ghidra on Linux and macOS, plus an experimental Windows x64 Ghidra P0 for approved native PE applications; execution-free managed PE/CLI triage; reproducible Evidence records; controlled process capture; passive website, Electron page, and Node/Electron V8 Inspector observation; JavaScript/source-map reconstruction; and provider-neutral graphs for connecting application layers without confusing static inference with runtime observation. The longer-term toolkit extends the same agent workflow to APIs, protocols, mobile artifacts, firmware, richer runtime behavior, and differences between versions.
|
||||
|
||||
Reverse engineering normally makes the operator choose a tool, learn its API, move evidence between programs, and decide what to inspect next. REA gives that work to the agent through commands, skills, structured results, and repeatable investigation workflows.
|
||||
|
||||
@@ -89,13 +89,13 @@ REA shows how it reached its conclusions. It does not claim to recover original
|
||||
### Run setup — recommended
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
The npm package-runner prompt, when shown, approves downloading REA for this
|
||||
invocation; it does not approve any setup changes. The REA wizard separately
|
||||
shows its complete plan and asks before applying it. Setup does not update
|
||||
Homebrew, Node.js, or npm. `npx rea-agents setup` opens with the work it
|
||||
Homebrew, Node.js, or npm. The setup command opens with the work it
|
||||
enables: investigate local apps from an agent, recover evidence through a
|
||||
deep-analysis provider, and reuse REA's guided workflow. It summarizes the
|
||||
detected agents, then asks which capabilities to set up: agent integration
|
||||
@@ -103,6 +103,11 @@ detected agents, then asks which capabilities to set up: agent integration
|
||||
Nothing is preselected. Choosing agent integration opens a second empty
|
||||
checklist for the specific detected agents that should receive a registration.
|
||||
|
||||
`@latest` makes the requested release explicit and asks npm for the release
|
||||
currently published under that tag. REA does not silently replace the package
|
||||
version npm selected. Intentional rollbacks therefore remain available through
|
||||
an exact package request.
|
||||
|
||||
REA keeps the journey inline so its history remains in the terminal. Selecting
|
||||
a capability does not select every detected target or authorize a change.
|
||||
Before anything changes, REA validates existing configuration, prints exact
|
||||
@@ -132,7 +137,7 @@ Pass installer options after `bash -s --`, for example `--dry-run`, `--no-setup`
|
||||
### With an agent — recommended
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
Choose Agent Integration in the reviewed setup plan. REA installs the pinned MCP
|
||||
@@ -144,7 +149,7 @@ Review the setup plan, approve it if appropriate, then describe the app or featu
|
||||
### From Terminal — no installation
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
npx -y rea-agents@latest doctor
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
@@ -221,10 +226,10 @@ REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Co
|
||||
|
||||
### Ghidra read-only analysis provider
|
||||
|
||||
The Ghidra adapter supports the exact official Ghidra 12.1.2 release with a 64-bit full JDK 21 on Linux x64. It also provides an experimental Windows x64 P0 limited to approved native x86-64 PE applications. Download and extract those projects yourself, then configure absolute paths:
|
||||
The Ghidra adapter supports the exact official Ghidra 12.1.4 release with a 64-bit full JDK 21 on Linux x64 and macOS x64/arm64. macOS additionally requires the matching native Ghidra decompiler; the adapter does not build or install it. The adapter also provides an experimental Windows x64 P0 limited to approved native x86-64 PE applications. Download and extract those projects yourself, then configure absolute paths:
|
||||
|
||||
```bash
|
||||
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.2_PUBLIC
|
||||
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
|
||||
export JAVA_HOME=/absolute/path/to/jdk-21 # optional when java and javac resolve from PATH
|
||||
rea doctor --json
|
||||
rea setup
|
||||
@@ -235,15 +240,17 @@ Doctor distinguishes missing configuration, a bad installation root, the wrong G
|
||||
|
||||
On Windows, set the same variables in PowerShell and run `rea doctor --json`; automated `rea setup` and Hopper installation remain unavailable. The P0 target boundary rejects DLLs, managed PE files, non-x86-64 images, mutable/hostile inputs, and non-PE formats. See the [Windows Ghidra P0 operations guide](docs/windows-ghidra-p0.md) for registration, exact limitations, CI evidence, and acceptance gates.
|
||||
|
||||
REA loads its packaged Java `HeadlessScript` with `-scriptPath`, copies and digest-verifies the target in an ephemeral runtime, enables `-readOnly` and `-deleteProject`, caps auto-analysis at 300 seconds with two CPUs and a 2 GiB Java heap, and authenticates every request. Linux uses a mode-0600 Unix socket. Windows P0 uses token-authenticated IPv4 loopback and a token-free endpoint record because Node path-based IPC does not connect to Java AF_UNIX sockets on Windows. The bridge verifies Ghidra's imported-byte SHA-256 before serving any operation.
|
||||
REA loads its packaged Java `HeadlessScript` with `-scriptPath`, copies and digest-verifies the target in an ephemeral runtime, enables `-readOnly` and `-deleteProject`, and authenticates every request. Auto-analysis completes before operations are served; startup has a deadline, while tool requests run until a result, caller cancellation, or provider shutdown. Linux and macOS use a mode-0600 Unix socket. Windows P0 uses token-authenticated IPv4 loopback and a token-free endpoint record because Node path-based IPC does not connect to Java AF_UNIX sockets on Windows. The bridge verifies Ghidra's imported-byte SHA-256 before serving any operation.
|
||||
|
||||
The Ghidra adapter declares 19 direct and enhanced operations. Its ten inventory operations are `list_documents`, `list_procedures`, `list_strings`, `list_names`, `list_segments`, `address_name`, `procedure_address`, `resolve_containing_procedure`, `search_procedures`, and `search_strings`. It also admits `procedure_info`, `procedure_pseudo_code`, `procedure_assembly`, `read_function_instructions`, `procedure_callers`, `procedure_callees`, `procedure_references`, `xrefs`, and `analyze_function`. `read_function_instructions` is the offset-paginated fast path for raw instruction windows: it does not invoke the decompiler or whole-program name/string inventories, and is also exposed as `rea instructions`. These capabilities enable the shared Swift/Objective-C inventory workflows, `binary_overview`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, `trace_feature`, and complete function dossiers. Default-space addresses are lowercase `0x` hexadecimal. Other spaces, including `EXTERNAL`, use `<percent-encoded-space>:0x<hex>`. Symbol results identify primary, dynamic, external, type, and source facts; procedures distinguish external functions and thunks; strings identify charset, missing-terminator state, byte length, and value truncation; memory-block ends are exclusive and permissions come directly from Ghidra.
|
||||
The Ghidra adapter declares 22 direct and enhanced operations. Its ten inventory operations are `list_documents`, `list_procedures`, `list_strings`, `list_names`, `list_segments`, `address_name`, `procedure_address`, `resolve_containing_procedure`, `search_procedures`, and `search_strings`. It also admits `procedure_info`, `procedure_pseudo_code`, `procedure_assembly`, `read_function_instructions`, `procedure_callers`, `procedure_callees`, `procedure_references`, `xrefs`, `analyze_function`, `inspect_native_instruction`, `resolve_native_call_targets`, and `inspect_native_data_type`. The exact-object primitives report instruction tokens, static call targets, and recovered database layouts; unsupported facets remain explicit. `trace_native_values` composes bounded decompiler-derived argument/parameter and return/output dependencies across resolved calls. `read_function_instructions` is the offset-paginated fast path for raw instruction windows: it does not invoke the decompiler or whole-program name/string inventories, and is also exposed as `rea instructions`. These capabilities enable the shared Swift/Objective-C inventory workflows, `binary_overview`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, `trace_feature`, and complete function dossiers. Default-space addresses are lowercase `0x` hexadecimal. Other spaces, including `EXTERNAL`, use `<percent-encoded-space>:0x<hex>`. Symbol results identify primary, dynamic, external, type, and source facts; procedures distinguish external functions and thunks; strings identify charset, missing-terminator state, byte length, and value truncation; memory-block ends are exclusive and permissions come directly from Ghidra.
|
||||
|
||||
The bridge serves operations only after auto-analysis completes. Each Program owns one persistent `DecompInterface`; a bounded 32-request FIFO serializes Ghidra API access, and every decompile has a 30-second native deadline. Reference results preserve Ghidra's call/jump/data/read/write/indirect/computed/external facts, while unresolved targetless flows remain explicitly unknown. Synthetic entry-point references without actionable memory sources are omitted. Pseudocode and assembly are provider-specific observations, not original source or Hopper-equivalent text. An analysis timeout, scan or inventory safety limit, request timeout, or oversized response fails explicitly instead of returning a partial result labeled complete.
|
||||
The bridge serves operations only after auto-analysis completes. Each Program owns one persistent `DecompInterface`, and a serial FIFO keeps Ghidra API access on the owning program's thread; it has no fixed queue length or per-operation deadline. Reference results preserve Ghidra's call/jump/data/read/write/indirect/computed/external facts, while unresolved targetless flows remain explicitly unknown. Synthetic entry-point references without actionable memory sources are omitted. Pseudocode and assembly are provider-specific observations, not original source or Hopper-equivalent text. Caller cancellation and provider shutdown remain available; provider results are returned without a fixed response-size ceiling.
|
||||
|
||||
`npm run verify:ghidra` compiles source-owned x86-64 debug and stripped ELF, AArch64 ELF, x86-64 PE, and x86-64 Mach-O fixtures. Against real Ghidra 12.1.2 it validates every admitted operation, direct and indirect calls, imports/exports/thunks, typed references, strings/xrefs, multi-block CFG, cancellation, deadlines, concurrency, malformed inputs, and complete process/project cleanup. Set `REA_CC`, `REA_CLANG`, or `REA_LLD_LINK` only when the corresponding compiler command is not on `PATH`.
|
||||
`npm run verify:ghidra` builds debug and stripped host-native fixtures (Linux x64 ELF or macOS Mach-O) and a native DWARF 4 type-layout object. Against real Ghidra 12.1.4 it validates every admitted operation, direct and indirect calls, imports/exports/thunks, typed references, strings/xrefs, multi-block CFG, cancellation, deadlines, concurrency, malformed inputs, and complete process/project cleanup. It needs a host C compiler and the Ghidra/JDK prerequisites above.
|
||||
|
||||
`npm run verify:ghidra:windows` uses a deterministic source-owned native x86-64 PE application and requires all 19 operations, target/snapshot/import digest linkage, authenticated loopback transport, and cleanup on a controlled Windows x64 Ghidra 12.1.2 runner. This proof does not establish Job Object ownership, private DACLs, or reparse-point-safe authority.
|
||||
`npm run verify:ghidra:cross-format` adds AArch64 ELF, x86-64 PE, and x86-64 Mach-O fixtures. This separate lane requires `clang`, LLD, and `lld-link`; use `REA_CLANG`, `REA_LLD`, or `REA_LLD_LINK` to select alternate command paths. It preflights the required toolchain before compiling fixtures.
|
||||
|
||||
`npm run verify:ghidra:windows` uses a deterministic source-owned native x86-64 PE application and requires the Windows P0 operations, target/snapshot/import digest linkage, authenticated loopback transport, and cleanup on a controlled Windows x64 Ghidra 12.1.4 runner. This proof does not establish Job Object ownership, private DACLs, or reparse-point-safe authority.
|
||||
|
||||
To remove only REA-owned MCP registrations and the managed skill:
|
||||
|
||||
@@ -252,7 +259,7 @@ rea uninstall
|
||||
rea uninstall --purge-data # also removes only ~/.rea/cache and ~/.rea/state
|
||||
```
|
||||
|
||||
Uninstall preserves Hopper, Node.js, evidence, captures, external evidence roots, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
|
||||
Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
|
||||
|
||||
### CLI or agent?
|
||||
|
||||
@@ -260,73 +267,54 @@ Uninstall preserves Hopper, Node.js, evidence, captures, external evidence roots
|
||||
| ---------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| Ask an agent to investigate an app and build a feature | Install the skill, then talk to your agent |
|
||||
| Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` |
|
||||
| Validate, canonicalize, or compare Evidence v2 bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` |
|
||||
| Run or resume a persistent two-version artifact analysis | `rea investigate-versions` |
|
||||
| Map a local JavaScript/Electron application without executing it | `rea analyze PATH --approved` or `rea analyze-javascript-application` |
|
||||
| Validate, canonicalize, or compare Evidence bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` |
|
||||
| Map a local JavaScript/Electron application without executing it | `rea analyze PATH` or `rea analyze-javascript-application` |
|
||||
| Reuse immutable analysis results without relaunching a provider | Pass `--snapshot /approved/path/analysis.json` to a deep-analysis command |
|
||||
| Import source as historical reference | `rea import-reference-source` |
|
||||
| Capture or compare controlled process behavior | `rea capture-process` or `rea compare-process-captures` |
|
||||
|
||||
Filesystem evidence commands and MCP file tools are disabled until the operator approves absolute roots:
|
||||
|
||||
```bash
|
||||
export REA_EVIDENCE_ROOTS_JSON='["/absolute/path/to/evidence"]'
|
||||
export REA_INVESTIGATION_INPUT_ROOTS_JSON='["/absolute/path/to/releases"]'
|
||||
rea evidence-import /absolute/path/to/evidence/bundle.json
|
||||
rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evidence/canonical.json
|
||||
rea compare /absolute/path/to/evidence/left.json /absolute/path/to/evidence/right.json
|
||||
rea investigate-versions /absolute/path/to/releases/v1 /absolute/path/to/releases/v2 /absolute/path/to/evidence/releases.json --yes --workspace-name releases
|
||||
rea analyze /absolute/path/to/releases/app.asar --approved --json
|
||||
rea analyze-javascript-application /absolute/path/to/releases/app.asar --approved --json
|
||||
```
|
||||
|
||||
JavaScript application analysis reads the selected directory or ASAR directly;
|
||||
the command uses the supplied path directly:
|
||||
|
||||
```bash
|
||||
rea analyze /absolute/path/to/releases/app.asar --json
|
||||
rea analyze-javascript-application /absolute/path/to/releases/app.asar --json
|
||||
```
|
||||
|
||||
For a directory or `.asar`, generic `rea analyze` automatically selects the
|
||||
static JavaScript application provider when neither `--provider` nor
|
||||
`--snapshot` is supplied. The dedicated command remains available for explicit
|
||||
format and resource-limit controls. Both routes require `--approved` and an
|
||||
administrator-approved investigation input root.
|
||||
`--snapshot` is supplied. Both routes return the analysis and its Evidence
|
||||
context inline.
|
||||
|
||||
If REA is registered with an MCP client, run approved `rea setup` while the
|
||||
explicit investigation-root variable is set, then restart that client. Setup
|
||||
copies this non-secret policy into the managed registration; changing the shell
|
||||
environment alone cannot update an already-running MCP process.
|
||||
|
||||
`investigate-versions` inventories both versions, checkpoints their observed
|
||||
Evidence, derives an artifact comparison, and records a changed-behavior report.
|
||||
Both input paths must resolve beneath `REA_INVESTIGATION_INPUT_ROOTS_JSON`;
|
||||
workspace files remain independently restricted by `REA_EVIDENCE_ROOTS_JSON`.
|
||||
The workspace uses deterministic content identities and monotonic CAS-linked
|
||||
revisions, so the same request resumes an interrupted run or reuses a completed
|
||||
run without replacing earlier investigations. It currently compares static
|
||||
artifact structure only; it does not execute either version, and its report
|
||||
keeps every difference labeled as a behavior candidate. See
|
||||
[Persistent investigation workspaces](docs/investigation-workspaces.md).
|
||||
|
||||
Historical source import requires a separate allowlist and never treats source as current behavioral authority:
|
||||
Historical source import takes the directory directly and never treats source
|
||||
as current behavioral authority:
|
||||
|
||||
```bash
|
||||
export REA_REFERENCE_ROOTS_JSON='["/absolute/path/to/source"]'
|
||||
rea import-reference-source /absolute/path/to/source
|
||||
```
|
||||
|
||||
Exports never replace an existing file unless `--overwrite` is explicit. Imports are size/depth bounded, validate every Evidence v2 ID and manifest, and never execute bundle content.
|
||||
Imports read the path supplied to the command and validate every Evidence ID and manifest. Exports never replace an existing file unless `--overwrite` is explicit.
|
||||
|
||||
Provider-neutral analysis snapshots persist successful, immutable REA calls and
|
||||
their Evidence v2 records. They are exact caches rather than Hopper databases:
|
||||
their Evidence records. They are exact caches rather than Hopper databases:
|
||||
REA reuses a v2 entry only when the binary digest, kind, format, architecture,
|
||||
operation parameters, concrete provider build, and canonical analysis-profile
|
||||
digest match. Hopper loader defaults and configured overrides are normalized by
|
||||
the Hopper adapter and committed to that profile, so overrides occupy a distinct
|
||||
safe cache partition instead of disabling snapshots. Snapshot v1 cannot prove
|
||||
those semantics and is rejected with recapture guidance. Cursor-dependent and
|
||||
safe cache partition instead of disabling snapshots. Cursor-dependent and
|
||||
mutating calls are never cached. Snapshot files can contain proprietary analysis
|
||||
results and local paths, so REA keeps them local, writes them with owner-only
|
||||
permissions, and requires a separate approved root:
|
||||
results and local paths, so REA keeps them local and writes them with owner-only
|
||||
permissions. The caller supplies the snapshot path directly:
|
||||
|
||||
```bash
|
||||
export REA_ANALYSIS_SNAPSHOT_ROOTS_JSON='["/absolute/path/to/analysis"]'
|
||||
rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
|
||||
# The same exact query can now be answered from the snapshot.
|
||||
# The same exact query can be answered from that snapshot.
|
||||
rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
|
||||
```
|
||||
|
||||
@@ -364,25 +352,26 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
|
||||
- Reconstruct an app's authentication, storage, update, or networking flow.
|
||||
- Recover enough structure to document an undocumented format or interface.
|
||||
- Trace a suspicious behavior from a string or symbol to the code that implements it.
|
||||
- Run, checkpoint, resume, and reuse a content-addressed artifact investigation across two versions.
|
||||
- Turn recovered behavior into product features, tests, migration notes, ports, or interoperable replacements.
|
||||
- Analyze Swift and Objective-C metadata without manually untangling every mangled symbol.
|
||||
- Leave names, comments, and bookmarks in Hopper so human and agent analysis reinforce each other.
|
||||
|
||||
See [native investigation](docs/native-investigation.md) for keyed archives, instruction/call/type primitives, typed dispatch metadata, value traces and approved native desktop observation.
|
||||
|
||||
## Tool catalog for investigation
|
||||
|
||||
| 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 |
|
||||
| Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
|
||||
| Artifact graph | 3 | bounded provider-neutral inspection, deterministic directory/ZIP/APK/IPA/MSIX/AppX/ASAR inventory, and explicitly selected extraction into an absent owned tree |
|
||||
| Managed PE/CLI | 8 | PE/CLI identity, metadata members, CIL hashes, P/Invoke/native-boundary declarations and verification, application-graph projection, decompiler reconstruction import, token remapping, runtime-correlation plans, and version comparison |
|
||||
| Browser observation | 9 | exact-origin passive CDP capture, bundle and source-map analysis, WebMCP discovery, session timelines, capture diff, visual evidence, and bounded Playwright scenarios |
|
||||
| Electron analysis | 4 | passive root-confined observation, bounded static application mapping, and evidence-backed static/runtime reconciliation |
|
||||
| JavaScript runtime | 2 | approved attach-only Node/Electron Inspector target discovery plus bounded script and execution-context observation without evaluation or instrumentation |
|
||||
| Application workflows | 12 | bounded cross-layer traces, unique-only version matching, historical-source to bundle mapping, static export return-shape comparison, approved Linux-isolated extracted-module replay, managed-runtime characterization, reconstruction coverage closure, deterministic obligation ledgers, and end-to-end readiness conformance |
|
||||
| Workspace and observation | 19 | target lifecycle, Evidence v2 bundles, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
|
||||
| Tool family | Count | Examples |
|
||||
| ------------------------- | ----: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native inspection | 39 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations, bounded byte reads, file-offset translation |
|
||||
| Investigation workflows | 15 | `binary_overview`, `analyze_function`, `inspect_native_api`, `inspect_native_dispatch_metadata`, `batch_decompile`, `trace_feature`, `trace_native_investigation`, exact string-to-code lookup, bounded call paths, call graphs, Swift and Objective-C discovery |
|
||||
| Native macOS utilities | 7 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
|
||||
| Artifact graph | 5 | complete inline inspection of directories and supported packages, compiled Interface Builder UI graph decoding, paginated Apple asset catalog metadata, plus explicitly selected extraction into an absent owned tree |
|
||||
| Managed PE/CLI | 8 | PE/CLI identity, metadata members, CIL hashes, P/Invoke/native-boundary declarations and verification, application-graph projection, decompiler reconstruction import, token remapping, runtime-correlation plans, and version comparison |
|
||||
| Browser observation | 9 | exact-origin passive CDP capture, bundle and source-map analysis, WebMCP discovery, session timelines, capture diff, visual evidence, and bounded Playwright scenarios |
|
||||
| Electron analysis | 5 | passive root-confined observation, static application mapping, evidence-backed static/runtime reconciliation, and provider-owned click/wait scenarios |
|
||||
| JavaScript runtime | 2 | approved attach-only Node/Electron Inspector target discovery plus bounded script and execution-context observation without evaluation or instrumentation |
|
||||
| Application workflows | 10 | complete cross-layer traces, unique-only version matching, historical-source to bundle mapping, static export return-shape comparison, approved Linux-isolated extracted-module replay, managed-runtime characterization, reconstruction coverage closure, and deterministic obligation ledgers |
|
||||
| Workspace and observation | 22 | target lifecycle, inline Evidence bundle retrieval, aggregate navigation/address context, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
|
||||
|
||||
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness implements controlled behavioral capture.
|
||||
|
||||
@@ -395,32 +384,31 @@ REA is already useful for native application, browser, and Electron investigatio
|
||||
- Attach to a user-owned Chrome-family browser over a configured loopback CDP endpoint; capture exact-origin web structure, safe metadata, approved value-free payload shapes, bundle/source-map evidence, WebMCP declarations, user-action timelines, capture diffs, and explicitly approved screenshots without navigation or JavaScript evaluation.
|
||||
- Inspect Electron `file://` renderer pages through a separate canonical-root permission boundary without invoking Electron APIs; script contents remain separately approved and byte bounded.
|
||||
- Attach to one exact approved Node or Electron V8 Inspector target and retain bounded `scriptParsed` plus execution-context lifecycle metadata without evaluation, breakpoints, resume, source reads, or instrumentation. require/import edges, EventEmitter activity, Electron IPC, PID identity, and role identity remain unknown. See [passive Node and Electron runtime observation](docs/javascript-runtime-observation.md).
|
||||
- Validate and canonically serialize a provider-neutral [JavaScript Application Graph v1](docs/javascript-application-graph.md) spanning packages, ASAR entries, Electron roles, JavaScript/source-map entities, browser/runtime instances, IPC, endpoints, storage, and native add-ons. This shipped domain contract performs no extraction or I/O by itself.
|
||||
- Reconstruct bounded static package, entrypoint, Webpack/Rspack module, import, worker, endpoint, storage, source-map, BrowserWindow, preload, contextBridge, IPC, utility-process, and native-add-on structure from an approved local directory or ASAR through `analyze_javascript_application` or `rea analyze-javascript-application`. The AST-only [application service](docs/javascript-artifact-reconstruction.md) never executes bootstrap code, pairs only unique exact literal IPC channels, and reports dynamic or ambiguous channels as unresolved.
|
||||
- Validate and canonically serialize a provider-neutral [JavaScript Application Graph](docs/javascript-application-graph.md) spanning packages, ASAR entries, Electron roles, JavaScript/source-map entities, browser/runtime instances, IPC, endpoints, storage, and native add-ons. This shipped domain contract performs no extraction or I/O by itself.
|
||||
- Reconstruct static package, entrypoint, Webpack/Rspack module, import, worker, endpoint, storage, source-map, BrowserWindow, preload, contextBridge, IPC, utility-process, and native-add-on structure from a selected local directory or ASAR through `analyze_javascript_application` or `rea analyze-javascript-application`. Results and Evidence context are returned inline. The AST-only [application service](docs/javascript-artifact-reconstruction.md) never executes bootstrap code, pairs only unique exact literal IPC channels, and reports dynamic or ambiguous channels as unresolved.
|
||||
- Reconcile that static graph with existing passive web or Electron Evidence through `reconcile_javascript_runtime` or `rea reconcile-javascript-runtime`. Exact captured bytes outrank caller-declared file/URL mappings; target, frame, script, worker, cache, and asset ambiguity stays explicit, source-map authority stays separate, and a module resident in an observed bundle is never reported as executed. See [JavaScript static/runtime reconciliation](docs/javascript-runtime-reconciliation.md).
|
||||
- Trace a literal route, string, API, IPC channel, module, or native export through authenticated application Evidence with explicit traversal bounds, then hand exact native artifact digests and requested exports to retained Ghidra or Hopper Evidence without automatic provider switching. Compare application versions using unique-only digest, source-map, structural, and semantic tiers; map a committed historical source inventory to bundle nodes with explicit digest and path scores; compare one exact JavaScript export's static return shapes through unique literal discriminants and bounded JSON Pointer changes. Duplicate, dynamic, incomplete, ambiguous, and truncated facts stay unknown. See [cross-layer JavaScript application workflows](docs/javascript-application-workflows.md).
|
||||
- Classify PE/CLI managed artifacts with `inspect_managed_artifact` / `rea inspect-managed-artifact`, inspect bounded metadata members, signatures, raw CIL hashes, limited decoded-instruction-tuple v1 hashes, separately reported exception regions, call edges, and field-access anchors with `inspect_managed_members` / `rea inspect-managed-members`, inventory declared ModuleRef/ImplMap/PInvoke and non-IL method boundary indicators with `inspect_managed_native_boundaries` / `rea inspect-managed-native-boundaries`, then compare two authenticated member observations with `compare_managed_members` / `rea compare-managed-members`. `verify_managed_native_boundaries` / `rea verify-managed-native-boundaries` checks managed P/Invoke declarations against authenticated native export or function Evidence while keeping verified, inferred, contradicted, and unresolved states distinct. The comparison treats build-local tokens as build-local and uses unique decoded-CIL/signature and structural method-shape tiers, never names alone; the v1 tuple hash does not itself resolve tokens or fully commit control flow. `project_managed_application_graph` / `rea project-managed-application-graph` projects authenticated managed artifact/member/native-boundary Evidence into the existing application graph for cross-layer feature tracing. `import_managed_reconstruction` / `rea import-managed-reconstruction` admits user-supplied decompiler C#/IL/pseudocode as analyst inference only after exact artifact SHA-256, MVID, signature, and the shipped v1 decoded-IL commitment match. Separately, `plan_managed_runtime_correlation` / `rea plan-managed-runtime-correlation` can admit a default-disabled, permission-gated runtime-correlation plan locked to the same build evidence. These paths never load the assembly, resolve CLR dependencies, execute target code, run a decompiler, or translate managed tokens into native addresses; full normalized-CIL v2 semantics, native-body bridge mapping, and an actual runtime executor remain future managed-code contracts.
|
||||
- Trace a literal route, string, API, IPC channel, module, or native export through the complete reachable graph in authenticated application Evidence, then hand exact native artifact digests and requested exports to retained Ghidra or Hopper Evidence without automatic provider switching. Compare application versions using unique-only digest, source-map, structural, and semantic tiers; map a committed historical source inventory to bundle nodes with explicit digest and path scores; compare one exact JavaScript export's static return shapes through unique literal discriminants and JSON Pointer changes. Duplicate, dynamic, incomplete, ambiguous, and truncated facts stay unknown. See [cross-layer JavaScript application workflows](docs/javascript-application-workflows.md).
|
||||
- Classify PE/CLI managed artifacts with `inspect_managed_artifact` / `rea inspect-managed-artifact`, inspect file-backed metadata members, signatures, raw CIL hashes, decoded-instruction-tuple fingerprints, separately reported exception regions, call edges, and field-access anchors with `inspect_managed_members` / `rea inspect-managed-members`, inventory declared ModuleRef/ImplMap/PInvoke and non-IL method boundary indicators with `inspect_managed_native_boundaries` / `rea inspect-managed-native-boundaries`, then compare two authenticated member observations with `compare_managed_members` / `rea compare-managed-members`. `verify_managed_native_boundaries` / `rea verify-managed-native-boundaries` checks managed P/Invoke declarations against authenticated native export or function Evidence while keeping verified, inferred, contradicted, and unresolved states distinct. The comparison treats build-local tokens as build-local and uses unique decoded-CIL/signature and structural method-shape tiers, never names alone; tuple fingerprints do not themselves resolve tokens or fully commit control flow. `project_managed_application_graph` / `rea project-managed-application-graph` projects authenticated managed artifact/member/native-boundary Evidence into the existing application graph for cross-layer feature tracing. `import_managed_reconstruction` / `rea import-managed-reconstruction` admits user-supplied decompiler C#/IL/pseudocode as analyst inference only after exact artifact SHA-256, MVID, signature, and decoded-IL commitments match. Separately, `plan_managed_runtime_correlation` / `rea plan-managed-runtime-correlation` can admit a default-disabled, permission-gated runtime-correlation plan locked to the same build evidence. These paths never load the assembly, resolve CLR dependencies, execute target code, run a decompiler, or translate managed tokens into native addresses; complete normalized-CIL semantics, native-body bridge mapping, and an actual runtime executor remain future managed-code contracts.
|
||||
- Configure `REA_ILSPY_CMD_PATH=/absolute/path/to/ilspycmd` only when you want
|
||||
doctor and `verify:managed` to inspect a bring-your-own ILSpy command as a
|
||||
real reconstruction oracle. REA does not install ILSpy and does not treat
|
||||
decompiler text as canonical metadata or CIL observation.
|
||||
- Traverse content-addressed artifact graphs without extraction; on macOS, read-only DMG traversal additionally requires `native_mount_approved: true` and `REA_ARTIFACT_NATIVE_MOUNT_ENABLED=true`. Materialize only approved occurrences into absent output roots.
|
||||
- Build bounded function dossiers with pseudocode, assembly, CFG edges, comments, calls, references, strings, and names.
|
||||
- Build function dossiers with pseudocode, assembly, CFG edges, comments, calls, references, strings, and names.
|
||||
- Search and trace features across symbols, strings, metadata, references, and call paths.
|
||||
- Record every successful result as deterministic Evidence v2 with artifact and provider identity, confidence, authority, limitations, and locations.
|
||||
- Record every successful result as deterministic Evidence with artifact and provider identity, confidence, authority, limitations, and locations.
|
||||
- Export and import evidence bundles across sessions.
|
||||
- Persist automatic cross-version artifact runs as canonical, lock-protected workspaces with tamper-evident revision commitments.
|
||||
- Capture approved PTY scenarios as Process Capture v4 Evidence, including committed run manifests, raw and rendered terminal frames, scripted interactions, descendant settlement, named filesystem checkpoints, deterministic command shims, and loopback HTTP/WebSocket exchanges.
|
||||
- Capture approved PTY scenarios as Process Capture Evidence, including committed run manifests, raw and rendered terminal frames, scripted interactions, descendant settlement, named filesystem checkpoints, deterministic command shims, and loopback HTTP/WebSocket exchanges.
|
||||
- Validate finite replay machines without launching a target through `run_replay_machine` or `rea run-replay-machine`; ordered events return typed decisions, actions, captured aliases, transition journals, final state, and exact limit use without echoing request or captured values.
|
||||
- Compare complete artifact inventories by stable path, content, metadata, and relations; incomplete evidence never implies equivalence.
|
||||
- Compare explicit function dossiers across text, calls, references, strings, and address-normalized CFG topology with per-facet unknowns.
|
||||
- Compare canonical Evidence bundles by exact membership, explicit observation pairs, and residual-unknown histories without turning omissions into behavioral absence.
|
||||
- Aggregate runtime comparisons into observed behavior changes while keeping static artifact/function differences labeled as candidates.
|
||||
- Build bounded, Evidence-cited direct call paths by exact address without treating missing dossiers as graph leaves.
|
||||
- Build Evidence-cited direct call paths by exact address without treating missing dossiers as graph leaves.
|
||||
- Correlate exact static/runtime findings through explicit hypotheses without claiming causality from cochange.
|
||||
- Verify finite behavioral and structural reconstruction specifications with pass, fail, and unknown kept distinct.
|
||||
- Track residual unknowns through immutable CAS revisions, evidence-qualified resolution, contradictions, probes, and validated dependency relationships.
|
||||
- With explicit `unknown_registry_approved: true`, record bounded trace/capture residuals, typed provider unavailability, and capture disagreements automatically.
|
||||
- Evidence-producing workflows record returned residual uncertainty as residual unknowns linked to their result Evidence. Errors without supporting Evidence do not create registry records.
|
||||
- Start six [guided MCP workflows](docs/mcp-prompts.md) with live, session-aware completion for documents, procedures, providers, evidence, captures, artifact IDs, and active unknowns.
|
||||
|
||||
Hopper is the first provider, not the boundary of the project. Some current workflows still require Hopper and macOS; every evidence record identifies the provider and limitations behind its result.
|
||||
@@ -438,7 +426,7 @@ rea list-browser-targets http://127.0.0.1:9222 --approved --json
|
||||
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID --approved --json
|
||||
```
|
||||
|
||||
All eight browser tools expose the same Evidence v2 contracts over CLI and MCP. Inspection is passive: REA does not evaluate page JavaScript, navigate, click, close the page, or close the browser. Query values, credentials, cookies, authorization headers, storage values, and raw JSON or WebSocket values are never retained. Separately approved captures can retain bounded redacted console primitives, value-free JSON/WebSocket shapes, script sources, accessibility text, or screenshot pixels. Existing activity before attach is explicitly unavailable. See [Website observation with CDP](docs/browser-observation.md) for browser startup, schemas, limits, and the threat model.
|
||||
All eight browser tools expose the same Evidence contracts over CLI and MCP. Inspection is passive: REA does not evaluate page JavaScript, navigate, click, close the page, or close the browser. Query values, credentials, cookies, authorization headers, storage values, and raw JSON or WebSocket values are never retained. Separately approved captures can retain bounded redacted console primitives, value-free JSON/WebSocket shapes, script sources, accessibility text, or screenshot pixels. Existing activity before attach is explicitly unavailable. See [Website observation with CDP](docs/browser-observation.md) for browser startup, schemas, limits, and the threat model.
|
||||
|
||||
### Controlled browser scenarios
|
||||
|
||||
@@ -474,22 +462,19 @@ Attach-only JavaScript runtime observation is separately disabled by default:
|
||||
|
||||
```bash
|
||||
export REA_V8_INSPECTOR_OBSERVE_ENABLED=true
|
||||
export REA_V8_INSPECTOR_ENDPOINTS_JSON='["http://127.0.0.1:9229"]'
|
||||
export REA_V8_INSPECTOR_FILE_ROOTS_JSON='["/absolute/path/to/app"]'
|
||||
export REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON='[]'
|
||||
|
||||
rea list-javascript-runtime-targets http://127.0.0.1:9229 --approved --json
|
||||
rea list-javascript-runtime-targets http://127.0.0.1:9229 --json
|
||||
rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
|
||||
--runtime-kind node --approved --json
|
||||
--runtime-kind node --json
|
||||
```
|
||||
|
||||
REA sends only `Runtime.enable` and `Debugger.enable`. It retains bounded,
|
||||
scope-authorized script locations and execution-context lifecycle events;
|
||||
REA sends only `Runtime.enable` and `Debugger.enable`. It retains validated
|
||||
script locations and execution-context lifecycle events;
|
||||
require/import edges, EventEmitter activity, Electron IPC, PID identity, and
|
||||
Electron role identity stay explicit unknowns. See
|
||||
[passive Node and Electron runtime observation](docs/javascript-runtime-observation.md).
|
||||
|
||||
Exact package, tool-family, provider, setup-client, public schema-version, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, generated schemas, and a clean TypeDoc render.
|
||||
Exact package, tool-family, provider, setup-client, schema, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, generated schemas, and a clean TypeDoc render.
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -502,14 +487,14 @@ REA is growing into a toolkit for understanding software across static artifacts
|
||||
|
||||
### Next
|
||||
|
||||
1. **Controlled replay conformance growth** — extend the shipped Linux extracted-module sandbox with more source-owned hostile fixtures and cross-kernel conformance; browser or Electron interaction remains a different future authority.
|
||||
2. **Broader application graph evidence** — extend authenticated cross-layer traces with additional static extractors and separately approved runtime authorities.
|
||||
1. **Controlled replay conformance growth** — extend the shipped Linux extracted-module sandbox with more source-owned hostile fixtures and cross-kernel conformance; browser and Electron scenarios remain separately permissioned authorities.
|
||||
2. **Broader application graph evidence** — extend authenticated cross-layer traces with additional static extractors and additional runtime authorities.
|
||||
3. **Professional managed-code analysis** — extend shipped PE/CLI triage, CIL evidence, managed/native declaration inventory, source-owned conformance, and obfuscation-resistant comparisons toward verified native-provider composition under the accepted [managed-code boundary](docs/managed-code-analysis.md).
|
||||
4. **Deterministic behavior harnesses** — extend process ownership, protocol fixtures, filesystem observation, reconnects, and cross-version behavioral comparison.
|
||||
|
||||
### Later
|
||||
|
||||
1. **Controlled application interaction** — evaluate separately authorized full-application driving without widening passive browser, Electron, or extracted-module replay authority.
|
||||
1. **Broader controlled application interaction** — extend the separately authorized browser and Electron scenario surfaces beyond the current bounded click/wait actions without widening passive observation or extracted-module replay authority.
|
||||
2. **Native runtime observation** — approval-gated LLDB, Frida, system logs, process/filesystem observers, and native API tracing.
|
||||
3. **Additional providers and targets** — evaluate IDA/Hex-Rays, Binary Ninja, Rizin, LIEF, Windows-native providers, mobile artifacts, firmware, document formats, and other software-defined systems.
|
||||
|
||||
@@ -517,6 +502,8 @@ New providers must produce the same evidence and safety metadata as existing cap
|
||||
|
||||
See the [static-analysis provider evaluation](docs/provider-evaluation.md) for the shipped Ghidra function-analysis boundary, remaining admission gates, and provider comparison matrix; [ADR-0001](docs/adr/0001-provider-selection-and-analysis-profiles.md) for binding, selection, profile, snapshot, and compatibility decisions; the [controlled replay guide](docs/controlled-javascript-replay.md) plus [ADR-0002](docs/adr/0002-controlled-replay-authority-and-sandbox.md) for the shipped JavaScript replay boundary; and [ADR-0003](docs/adr/0003-managed-code-evidence-and-provider-boundary.md) for the managed-code evidence and provider design.
|
||||
|
||||
See the [native UI and dispatch investigation guide](docs/native-investigation.md) for compiled Interface Builder decoding, symbol-derived metadata limits, and the current p-code value-flow boundary.
|
||||
|
||||
## Using REA with other agents
|
||||
|
||||
Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. It automatically configures the first six when present; detected Devin installations are reported but left unchanged. Any agent that supports local MCP servers can use REA with the configuration below.
|
||||
@@ -530,7 +517,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@2.5.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -556,7 +543,6 @@ current session for bounded `completion/complete` suggestions; see
|
||||
flowchart LR
|
||||
Agent["Agent"] --> REA["REA<br/>CLI + MCP"]
|
||||
Terminal --> REA
|
||||
REA --> Workspace["Investigation workspace<br/>evidence + artifacts + captures"]
|
||||
REA --> Session["Target-bound session router"]
|
||||
Session --> Registry["Deep-provider registry<br/>deterministic selection"]
|
||||
Registry --> Hopper["Hopper provider"]
|
||||
@@ -573,7 +559,7 @@ flowchart LR
|
||||
Artifact --> Target
|
||||
```
|
||||
|
||||
The CLI and MCP server use the same application workflows and evidence contracts. A provider declares which capabilities it supports and the side effects those capabilities may have. Terminal commands are short-lived; an MCP session can retain an active target and evidence ledger across an investigation. Approved persistent workspaces keep canonical Evidence and resumable run checkpoints across both process and session lifetimes.
|
||||
The CLI and MCP server use the same application workflows and evidence contracts. A provider declares which capabilities it supports and the side effects those capabilities may have. Terminal commands are short-lived; an MCP session can retain an active target and evidence ledger for the session.
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -582,20 +568,18 @@ The agent workflow above is the easiest way to use REA. For a one-off overview f
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
npx -y rea-agents@latest inspect /Applications/Notes.app
|
||||
npx -y rea-agents@latest inspect /Applications/Notes.app --detail detailed --limit 20
|
||||
npx -y rea-agents@latest search /Applications/Notes.app "offline"
|
||||
npx -y rea-agents@latest function /Applications/Notes.app 0x1000
|
||||
npx -y rea-agents@latest xrefs /Applications/Notes.app 0x1000
|
||||
npx -y rea-agents@latest trace /Applications/Notes.app "offline"
|
||||
npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json
|
||||
npx -y rea-agents@latest investigate-versions /path/to/v1 /path/to/v2 /absolute/path/to/evidence/releases.json --yes
|
||||
npx -y rea-agents@latest capabilities
|
||||
npx -y rea-agents@latest providers
|
||||
```
|
||||
|
||||
Run `npx -y rea-agents@latest --help` for direct decompilation, bounded search and
|
||||
other options. `analyze` and `inspect` share the same overview workflow;
|
||||
`function`, `xrefs`, and `trace` return the same Evidence v2 envelopes as MCP.
|
||||
`function`, `xrefs`, and `trace` return the same Evidence envelopes as MCP.
|
||||
|
||||
Or install the `rea` command globally:
|
||||
|
||||
@@ -662,7 +646,7 @@ silent fallback.
|
||||
|
||||
| Status | Meaning |
|
||||
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `0` | The requested operation completed. Truthful unknowns, warnings, and bounded or truncated evidence remain successful results. |
|
||||
| `0` | The requested operation completed. Truthful unknowns, warnings, and partial or truncated evidence remain successful results. |
|
||||
| `1` | Arguments, policy, permission, provider analysis, integrity checking, cancellation, timeout, setup, diagnostics, update, uninstall, output encoding, or output writing prevented completion. Structured output identifies the failure category when REA could encode it. |
|
||||
| `128 + N` | The process ended from signal `N`, where the shell or runtime preserves the conventional signal-derived status. |
|
||||
|
||||
@@ -683,11 +667,11 @@ rea inventory-artifact ./app.asar --json | jq . > inventory.json
|
||||
|
||||
REA starts Hopper when needed; Hopper does not need to be running first. Hopper's launcher internally activates the application, so opening a target may bring Hopper to the foreground. REA asks macOS to start Hopper hidden and in the background when possible, but cannot guarantee that it will remain behind the current application.
|
||||
|
||||
REA derives explicit format and architecture arguments to prevent common FAT and ARM selection dialogs. Other Hopper or macOS dialogs may still require a person. REA reports timeouts and remediation through CLI or MCP results instead of attempting to answer UI prompts.
|
||||
REA derives explicit format and architecture arguments to prevent common FAT and ARM selection dialogs. Other Hopper or macOS dialogs may still require a person. REA reports startup failures and remediation through CLI or MCP results instead of attempting to answer UI prompts.
|
||||
|
||||
Hopper bridge calls pass through a bounded serial FIFO because Hopper's Python API runs on one dedicated thread. Queue wait counts against the caller deadline; a timeout settles the caller but does not release the wire slot until Hopper actually replies. `binary_session.analysis_activity` exposes that detached work and MCP progress reports elapsed time. Successful decompilation text is cached per document and procedure, shared by pseudocode and dossier requests, and invalidated after rename or comment mutation.
|
||||
Hopper bridge calls pass through a serial FIFO because Hopper's Python API runs on one dedicated thread. Calls wait for a response or caller cancellation. Cancelling an active call settles that caller but keeps its wire slot until Hopper replies, preserving request/response correlation; `binary_session.analysis_activity` exposes active work and MCP progress reports elapsed time. Successful decompilation text is cached per document and procedure, shared by pseudocode and dossier requests, and invalidated after rename or comment mutation.
|
||||
|
||||
Hopper's synchronous public Python calls cannot stream a partial response after the caller deadline. Use `read_function_instructions` or `rea instructions` when raw instruction orientation is enough: this path does not decompile or scan whole-program name/string inventories. If a dossier has already timed out, inspect `analysis_activity`; do not assume a raw request can enter the serial bridge until that detached Hopper call actually returns.
|
||||
Hopper's synchronous public Python calls return when the operation completes; callers may cancel their wait. Use `read_function_instructions` or `rea instructions` when raw instruction orientation answers the question, without requesting a decompilation or whole-program name/string inventory.
|
||||
|
||||
Closing a REA session shuts down its bridge and removes its private socket directory. It does not quit a Hopper application the user may be using. If shutdown or cleanup cannot be verified, `close_binary` returns `cleanup_incomplete` with the affected local resources.
|
||||
|
||||
@@ -704,7 +688,7 @@ Set `REA_PROCESS_CAPTURE_AUTO_GRANT=false` to configure those process-capture
|
||||
limits as a ceiling without implicitly granting them. This mode remains
|
||||
fail-closed until a narrower grant is established.
|
||||
|
||||
Capture a scenario or compare two saved Process Capture v4 Evidence records:
|
||||
Capture a scenario or compare two saved Process Capture Evidence records:
|
||||
|
||||
```bash
|
||||
rea capture-process ./scenario.json > authority.json
|
||||
@@ -714,7 +698,7 @@ rea compare-process-captures authority.json reconstruction.json
|
||||
|
||||
The comparison reports each observed dimension separately and identifies the
|
||||
first terminal, interaction, exit, filesystem, protocol, process, or shim
|
||||
divergence. See [Process Capture v4](docs/process-capture.md) for scenario
|
||||
divergence. See [Process Capture](docs/process-capture.md) for scenario
|
||||
fields, command-shim replay, checkpoint triggers, limits, and safety behavior.
|
||||
|
||||
REA installs a prebuilt PTY backend for supported macOS, Linux, and Windows
|
||||
@@ -732,7 +716,7 @@ verified or absent.
|
||||
|
||||
## Security model
|
||||
|
||||
REA does not provide a hosted analysis service. Hopper and Linux Ghidra bridge communication uses authenticated private local sockets. Windows Ghidra P0 uses authenticated IPv4 loopback but does not claim named-pipe DACL or hostile-local-user isolation. Dynamic capabilities are disabled by default and require both operator policy and explicit per-call approval. Shipped providers, passive observers, and Process Capture are not security sandboxes: providers and launched targets run with the current user's permissions. Extracted JavaScript replay is a distinct Linux-only capability that fails closed unless Bubblewrap namespaces, architecture-checked seccomp, private runtime mounts, and delegated cgroup limits are available; it never inherits browser, Electron, or Process Capture authority. See [ADR-0002](docs/adr/0002-controlled-replay-authority-and-sandbox.md). Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
|
||||
REA does not provide a hosted analysis service. Hopper and POSIX Ghidra bridge communication uses authenticated private local sockets. Windows Ghidra P0 uses authenticated IPv4 loopback but does not claim named-pipe DACL or hostile-local-user isolation. Dynamic capabilities are disabled by default and require both operator policy and explicit per-call approval. Shipped providers, passive observers, and Process Capture are not security sandboxes: providers and launched targets run with the current user's permissions. Extracted JavaScript replay is a distinct Linux-only capability that fails closed unless Bubblewrap namespaces, architecture-checked seccomp, private runtime mounts, and delegated cgroup limits are available; it never inherits browser, Electron, or Process Capture authority. See [ADR-0002](docs/adr/0002-controlled-replay-authority-and-sandbox.md). Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -760,7 +744,7 @@ No. Setup can install Hopper for you, but Hopper remains separate software with
|
||||
<details>
|
||||
<summary><strong>Does REA install or include Ghidra or Java?</strong></summary>
|
||||
|
||||
No. Ghidra support is bring-your-own. REA packages only its Java bridge source, validates the exact supported Ghidra 12.1.2 and 64-bit JDK 21 installation, and loads that bridge through Ghidra's external script path after setup approval records the paths.
|
||||
No. Ghidra support is bring-your-own. REA packages only its Java bridge source, validates the exact supported Ghidra 12.1.4 and 64-bit JDK 21 installation, and loads that bridge through Ghidra's external script path after setup approval records the paths.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -787,7 +771,10 @@ Any agent that can run a local MCP server can use the manual configuration. Setu
|
||||
|
||||
## Development
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, tests, and release instructions. PR CI publishes generated API documentation as the `api-docs` workflow artifact.
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, and release
|
||||
instructions, and [docs/testing.md](docs/testing.md) for the behavioral test
|
||||
depths, focused developer commands, coverage floors, and CI evidence. PR CI
|
||||
publishes generated API documentation as the `api-docs` workflow artifact.
|
||||
|
||||
`npm run verify:agent` runs brandless native, JavaScript-application, managed,
|
||||
and browser prompts through a real local Codex CLI. Its JSON report measures
|
||||
@@ -795,7 +782,7 @@ natural MCP use, first-tool routing, repeated calls, actual Codex token usage,
|
||||
completion quality, and explicit treatment of authority and unknowns.
|
||||
|
||||
`npm run evidence:generate` regenerates the managed conformance manifest and
|
||||
Evidence v2 completion ledger from live verifier output. `npm run
|
||||
Evidence completion ledger from live verifier output. `npm run
|
||||
evidence:check` reruns the verifier and fails when artifacts, scenarios,
|
||||
providers, schemas, claim counts, Evidence IDs, or the bundled skill have
|
||||
drifted. Unsupported claims remain explicit and never count as passes.
|
||||
|
||||
+17
-17
@@ -65,7 +65,7 @@ npx rea-agents setup
|
||||
### باستخدام وكيل برمجة — موصى به
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
اطلب من الوكيل إعداد REA. سيفحص جهاز Mac ويشرح ما يحتاج إلى تثبيته ويطلب الموافقة ويرشدك خلال رسائل النظام. أعد تشغيل الوكيل بعد الإعداد إذا طلب ذلك لتحميل مجموعة الأدوات الكاملة.
|
||||
@@ -78,7 +78,7 @@ npx rea-agents setup
|
||||
|
||||
يعرض `rea setup` خطة التغييرات كاملة ويطلب التأكيد قبل تطبيقها. لا يثبت أو يحدّث Homebrew أو Node.js أو npm. إذا كان [Hopper](https://www.hopperapp.com/) مفقودًا، يقترح Setup الحزمة الرسمية. Hopper برنامج منفصل يحتاج إلى ترخيص خاص به.
|
||||
|
||||
إذا كان Ghidra 12.1.2 PUBLIC وJDK 21 الكامل بنواة 64 بت مثبتين مسبقًا على Linux 64 بت، فيمكن لـ Setup أيضًا تسجيل `GHIDRA_INSTALL_DIR` و`JAVA_HOME` الاختياري بعد الموافقة. لا ينزّل REA أو يثبت أو يعدّل Ghidra أو Java. يوفّر مزوّد Ghidra ضمن جلسة headless معزولة وللقراءة فقط الجرد، وفك الترجمة، والتجميع، وعلاقات الاستدعاء، والمراجع ذات الأنواع، وxref، وCFG، وملفات تحليل الدوال؛ وتبقى حالة الواجهة الرسومية وعمليات التعديل غير متاحة.
|
||||
إذا كان Ghidra 12.1.4 PUBLIC وJDK 21 الكامل بنواة 64 بت مثبتين مسبقًا على Linux 64 بت، فيمكن لـ Setup أيضًا تسجيل `GHIDRA_INSTALL_DIR` و`JAVA_HOME` الاختياري بعد الموافقة. لا ينزّل REA أو يثبت أو يعدّل Ghidra أو Java. يوفّر مزوّد Ghidra ضمن جلسة headless معزولة وللقراءة فقط الجرد، وفك الترجمة، والتجميع، وعلاقات الاستدعاء، والمراجع ذات الأنواع، وxref، وCFG، وملفات تحليل الدوال؛ وتبقى حالة الواجهة الرسومية وعمليات التعديل غير متاحة.
|
||||
|
||||
#### التثبيت على Linux واستكشاف الأخطاء
|
||||
|
||||
@@ -90,7 +90,7 @@ npx rea-agents setup
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
إذا طلب macOS أو برنامج التثبيت تأكيدًا، فأكمل الخطوة ثم شغّل الأمر نفسه مرة أخرى.
|
||||
@@ -159,18 +159,18 @@ rea uninstall --purge-data
|
||||
|
||||
## منصة أدوات التحقيق
|
||||
|
||||
| عائلة الأدوات | العدد | أمثلة |
|
||||
| --------------------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| فحص الملفات التنفيذية | 36 | الدوال، والشيفرة شبه المصدرية، والتعليمات، والسلاسل، والأسماء، والمراجع، والتعليقات |
|
||||
| التحليل المركب | 12 | النظرة العامة، وفك الترجمة الدفعي، ومخططات الاستدعاء، والمراجع، واكتشاف الأنواع |
|
||||
| أدوات macOS الأصلية | 5 | بيانات Mach-O والتوقيعات وملفات plist والمعماريات وفك رموز Swift دون تشغيل Hopper |
|
||||
| رسم القطع الأثرية | 3 | جرد حتمي للدلائل وZIP/APK/IPA وASAR واستخراج معاملاتي محدد صراحة |
|
||||
| Managed PE/CLI | 8 | هوية PE/CLI، أعضاء البيانات الوصفية، بصمات CIL، تصريحات P/Invoke/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
|
||||
| مراقبة المتصفح | 9 | التقاط CDP محدود الأصل وتحليل الحزم وخرائط المصدر واكتشاف WebMCP والفروق والأدلة المرئية |
|
||||
| تحليل Electron | 4 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
|
||||
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector المعتمدة للاتصال فقط، ومراقبة محدودة للبرامج النصية وسياقات التنفيذ دون تقييم أو تجهيز |
|
||||
| سير عمل التطبيقات | 12 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية والتحقق الشامل من جاهزية إعادة البناء |
|
||||
| جلسة الملف التنفيذي | 19 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
|
||||
| عائلة الأدوات | العدد | أمثلة |
|
||||
| --------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| فحص الملفات التنفيذية | 39 | الدوال، والشيفرة شبه المصدرية، والتعليمات، والسلاسل، والأسماء، والمراجع، والتعليقات |
|
||||
| التحليل المركب | 15 | النظرة العامة، و`inspect_native_api`، وفك الترجمة الدفعي، ومخططات الاستدعاء، والمراجع، واكتشاف الأنواع |
|
||||
| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وملفات plist والمعماريات وفك رموز Swift دون تشغيل Hopper |
|
||||
| رسم القطع الأثرية | 5 | فحص الدلائل والحزم، ورسم واجهة Interface Builder، وبيانات كتالوج أصول Apple، والاستخراج المحدد صراحة |
|
||||
| Managed PE/CLI | 8 | هوية PE/CLI، أعضاء البيانات الوصفية، بصمات CIL، تصريحات P/Invoke/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
|
||||
| مراقبة المتصفح | 9 | التقاط CDP محدود الأصل وتحليل الحزم وخرائط المصدر واكتشاف WebMCP والفروق والأدلة المرئية |
|
||||
| تحليل Electron | 5 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
|
||||
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector المعتمدة للاتصال فقط، ومراقبة محدودة للبرامج النصية وسياقات التنفيذ دون تقييم أو تجهيز |
|
||||
| سير عمل التطبيقات | 10 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية |
|
||||
| جلسة الملف التنفيذي | 22 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
|
||||
|
||||
## استخدام REA مع وكلاء برمجة آخرين
|
||||
|
||||
@@ -183,7 +183,7 @@ rea uninstall --purge-data
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@2.5.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,7 +258,7 @@ export HOPPER_LOADER_ARGS_JSON='["-l", "Mach-O", "--aarch64"]'
|
||||
<details>
|
||||
<summary><strong>هل يعمل REA على Linux أو Windows؟</strong></summary>
|
||||
|
||||
نعم، يعمل REA على macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux 64 بت؛ Windows غير مدعوم حاليًا. يستخدم التحليل العميق Hopper أو مزوّد Ghidra للقراءة فقط الذي يختاره المستدعي؛ ويتطلب Ghidra على Linux الإصدار 12.1.2 وJDK 21 الكامل.
|
||||
نعم، يعمل REA على macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux 64 بت؛ Windows غير مدعوم حاليًا. يستخدم التحليل العميق Hopper أو مزوّد Ghidra للقراءة فقط الذي يختاره المستدعي؛ ويتطلب Ghidra على Linux الإصدار 12.1.4 وJDK 21 الكامل.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+16
-16
@@ -67,7 +67,7 @@ REA は調査をバイナリ上の根拠に結び付けます。元のソース
|
||||
### コーディングエージェントから(推奨)
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
エージェントに REA のセットアップを依頼してください。Mac を確認し、必要なインストールを説明して承認を求め、システムプロンプトを案内します。完全なツールを読み込むため再起動を求められた場合は、セットアップ後に再起動してください。
|
||||
@@ -80,7 +80,7 @@ npx rea-agents setup
|
||||
|
||||
`rea setup` は変更計画を表示し、確認後に適用します。Homebrew、Node.js、npm をインストールまたは更新しません。[Hopper](https://www.hopperapp.com/) がない場合は公式パッケージを提案します。Hopper は別製品で、ライセンスは別途必要です。
|
||||
|
||||
64 ビット Linux に Ghidra 12.1.2 PUBLIC と完全な 64 ビット JDK 21 がすでにある場合、Setup は承認後に `GHIDRA_INSTALL_DIR` と任意の `JAVA_HOME` も登録できます。REA は Ghidra や Java をダウンロード、インストール、変更しません。Ghidra プロバイダーは隔離された読み取り専用 headless セッションで、インベントリ、逆コンパイル、アセンブリ、呼び出し関係、型付き参照、xref、CFG、関数 dossier を提供します。GUI 状態と変更操作は引き続き利用できません。
|
||||
64 ビット Linux に Ghidra 12.1.4 PUBLIC と完全な 64 ビット JDK 21 がすでにある場合、Setup は承認後に `GHIDRA_INSTALL_DIR` と任意の `JAVA_HOME` も登録できます。REA は Ghidra や Java をダウンロード、インストール、変更しません。Ghidra プロバイダーは隔離された読み取り専用 headless セッションで、インベントリ、逆コンパイル、アセンブリ、呼び出し関係、型付き参照、xref、CFG、関数 dossier を提供します。GUI 状態と変更操作は引き続き利用できません。
|
||||
|
||||
#### Linux のインストールとトラブルシューティング
|
||||
|
||||
@@ -91,7 +91,7 @@ Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux では、REA が公式の D
|
||||
```bash
|
||||
# 1. REA をセットアップ
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
macOS やインストーラーから確認を求められた場合は、その操作を完了してから同じコマンドをもう一度実行してください。
|
||||
@@ -149,18 +149,18 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
|
||||
|
||||
## 調査ツールカタログ
|
||||
|
||||
| ツール群 | 数 | 例 |
|
||||
| ---------------------- | --: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| バイナリ調査 | 36 | プロシージャ、疑似コード、アセンブリ、文字列、名前、セグメント、callers、callees、xrefs、注釈 |
|
||||
| 合成解析 | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift / ObjC 検出 |
|
||||
| macOS ネイティブ | 5 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift デマングル。Hopper 起動不要 |
|
||||
| アーティファクトグラフ | 3 | ディレクトリ、ZIP/APK/IPA、ASAR の決定的一覧と明示選択されたトランザクション抽出 |
|
||||
| Managed PE/CLI | 8 | PE/CLI 識別、メタデータメンバー、CIL ハッシュ、P/Invoke/ネイティブ境界宣言と検証、アプリケーショングラフ投影、逆コンパイル再構成インポート、構造的 token 再マッピング、ランタイム相関計画、バージョン比較 |
|
||||
| ブラウザ観察 | 9 | origin 限定 CDP 取得、bundle/source map 解析、WebMCP 検出、セッション、capture diff、視覚証拠 |
|
||||
| Electron 解析 | 4 | canonical ファイルルート内の受動観察、有界な静的アプリマッピング、Evidence に基づく静的/実行時の照合 |
|
||||
| JavaScript ランタイム | 2 | 承認済みのアタッチ専用 Node/Electron Inspector ターゲット検出と、評価や計装を行わない有界なスクリプト/実行コンテキスト観察 |
|
||||
| アプリケーション | 12 | 有界なクロスレイヤー追跡、一意限定のバージョン比較、静的なエクスポート戻り値形状比較、承認済み Linux 隔離 extracted-module replay、マネージドランタイム特性評価、再構築カバレッジ閉包、決定論的な義務台帳、エンドツーエンドの準備適合性検証 |
|
||||
| バイナリセッション | 19 | `open_binary`、`binary_session`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
|
||||
| ツール群 | 数 | 例 |
|
||||
| ---------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| バイナリ調査 | 39 | プロシージャ、疑似コード、アセンブリ、文字列、名前、セグメント、callers、callees、xrefs、注釈 |
|
||||
| 合成解析 | 15 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift / ObjC 検出 |
|
||||
| macOS ネイティブ | 7 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift デマングル。Hopper 起動不要 |
|
||||
| アーティファクトグラフ | 5 | ディレクトリと対応パッケージの調査、Interface Builder UI グラフ、Apple アセットカタログのメタデータ、明示選択された抽出 |
|
||||
| Managed PE/CLI | 8 | PE/CLI 識別、メタデータメンバー、CIL ハッシュ、P/Invoke/ネイティブ境界宣言と検証、アプリケーショングラフ投影、逆コンパイル再構成インポート、構造的 token 再マッピング、ランタイム相関計画、バージョン比較 |
|
||||
| ブラウザ観察 | 9 | origin 限定 CDP 取得、bundle/source map 解析、WebMCP 検出、セッション、capture diff、視覚証拠 |
|
||||
| Electron 解析 | 5 | canonical ファイルルート内の受動観察、有界な静的アプリマッピング、Evidence に基づく静的/実行時の照合 |
|
||||
| JavaScript ランタイム | 2 | 承認済みのアタッチ専用 Node/Electron Inspector ターゲット検出と、評価や計装を行わない有界なスクリプト/実行コンテキスト観察 |
|
||||
| アプリケーション | 10 | 有界なクロスレイヤー追跡、一意限定のバージョン比較、静的なエクスポート戻り値形状比較、承認済み Linux 隔離 extracted-module replay、マネージドランタイム特性評価、再構築カバレッジ閉包、決定論的な義務台帳 |
|
||||
| バイナリセッション | 22 | `open_binary`、`binary_session`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
|
||||
|
||||
## 他のコーディングエージェントで使う
|
||||
|
||||
@@ -173,7 +173,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@2.5.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-16
@@ -67,7 +67,7 @@ REA는 분석을 바이너리 증거에 근거하게 합니다. 원본 소스
|
||||
### 코딩 에이전트에서 시작하기(권장)
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
에이전트에게 REA 설정을 요청하세요. Mac을 확인하고 필요한 설치 내용을 설명한 뒤 승인을 요청하며 시스템 프롬프트를 안내합니다. 전체 도구를 불러오기 위해 재시작하라고 하면 설정 후 에이전트를 재시작하세요.
|
||||
@@ -80,7 +80,7 @@ npx rea-agents setup
|
||||
|
||||
`rea setup`은 전체 변경 계획을 표시하고 확인 후 적용합니다. Homebrew, Node.js 또는 npm을 설치하거나 업데이트하지 않습니다. [Hopper](https://www.hopperapp.com/)가 없으면 공식 패키지 설치를 제안합니다. Hopper는 별도 라이선스가 필요한 독립 소프트웨어입니다.
|
||||
|
||||
64비트 Linux에 Ghidra 12.1.2 PUBLIC과 완전한 64비트 JDK 21이 이미 설치되어 있다면 Setup은 승인 후 `GHIDRA_INSTALL_DIR`과 선택 사항인 `JAVA_HOME`도 등록할 수 있습니다. REA는 Ghidra나 Java를 다운로드, 설치 또는 수정하지 않습니다. Ghidra 공급자는 격리된 읽기 전용 headless 세션에서 인벤토리, 디컴파일, 어셈블리, 호출 관계, 형식이 지정된 참조, xref, CFG 및 함수 dossier를 제공합니다. GUI 상태와 변경 작업은 계속 사용할 수 없습니다.
|
||||
64비트 Linux에 Ghidra 12.1.4 PUBLIC과 완전한 64비트 JDK 21이 이미 설치되어 있다면 Setup은 승인 후 `GHIDRA_INSTALL_DIR`과 선택 사항인 `JAVA_HOME`도 등록할 수 있습니다. REA는 Ghidra나 Java를 다운로드, 설치 또는 수정하지 않습니다. Ghidra 공급자는 격리된 읽기 전용 headless 세션에서 인벤토리, 디컴파일, 어셈블리, 호출 관계, 형식이 지정된 참조, xref, CFG 및 함수 dossier를 제공합니다. GUI 상태와 변경 작업은 계속 사용할 수 없습니다.
|
||||
|
||||
#### Linux 설치 및 문제 해결
|
||||
|
||||
@@ -91,7 +91,7 @@ Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux에서 REA는 공식 DEB, RPM 또
|
||||
```bash
|
||||
# 1. REA 설정
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
macOS나 설치 프로그램이 확인을 요청하면 해당 절차를 완료하고 같은 명령을 다시 실행하세요.
|
||||
@@ -149,18 +149,18 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
|
||||
|
||||
## 조사 도구 카탈로그
|
||||
|
||||
| 도구 그룹 | 수 | 예시 |
|
||||
| ------------------- | --: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 바이너리 검사 | 36 | 프로시저, 의사 코드, 어셈블리, 문자열, 이름, 세그먼트, callers, callees, xrefs, 주석 |
|
||||
| 합성 분석 | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift 및 ObjC 탐색 |
|
||||
| macOS 네이티브 도구 | 5 | Mach-O 메타데이터, 코드 서명, plist, 아키텍처, Swift 디맹글링. Hopper 실행 불필요 |
|
||||
| 아티팩트 그래프 | 3 | 디렉터리, ZIP/APK/IPA, ASAR 결정적 인벤토리와 명시적으로 선택한 트랜잭션 추출 |
|
||||
| Managed PE/CLI | 8 | PE/CLI ID, 메타데이터 멤버, CIL 해시, P/Invoke/네이티브 경계 선언 및 검증, 애플리케이션 그래프 투영, 디컴파일 재구성 가져오기, 구조적 token 재매핑, 런타임 상관 계획, 버전 비교 |
|
||||
| 브라우저 관찰 | 9 | origin 제한 CDP 캡처, bundle/source map 분석, WebMCP 검색, 세션 타임라인, capture diff 및 시각 증거 |
|
||||
| Electron 분석 | 4 | canonical 파일 루트 내 수동 관찰, 제한된 정적 앱 매핑, Evidence 기반 정적/런타임 조정 |
|
||||
| JavaScript 런타임 | 2 | 승인된 연결 전용 Node/Electron Inspector 대상 탐색과 평가·계측 없는 제한된 스크립트 및 실행 컨텍스트 관찰 |
|
||||
| 애플리케이션 | 12 | 제한된 교차 계층 추적, 고유 일치 버전 비교, 정적 export 반환 형태 비교, 승인된 Linux 격리 extracted-module replay, 관리형 런타임 특성화, 재구성 커버리지 폐쇄, 결정론적 의무 원장, 엔드투엔드 준비 적합성 검증 |
|
||||
| 바이너리 세션 | 19 | `open_binary`, `binary_session`, 증거 번들, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
|
||||
| 도구 그룹 | 수 | 예시 |
|
||||
| ------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 바이너리 검사 | 39 | 프로시저, 의사 코드, 어셈블리, 문자열, 이름, 세그먼트, callers, callees, xrefs, 주석 |
|
||||
| 합성 분석 | 15 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift 및 ObjC 탐색 |
|
||||
| macOS 네이티브 도구 | 7 | Mach-O 메타데이터, 코드 서명, plist, 아키텍처, Swift 디맹글링. Hopper 실행 불필요 |
|
||||
| 아티팩트 그래프 | 5 | 디렉터리와 지원 패키지 검사, Interface Builder UI 그래프, Apple 에셋 카탈로그 메타데이터, 명시적으로 선택한 추출 |
|
||||
| Managed PE/CLI | 8 | PE/CLI ID, 메타데이터 멤버, CIL 해시, P/Invoke/네이티브 경계 선언 및 검증, 애플리케이션 그래프 투영, 디컴파일 재구성 가져오기, 구조적 token 재매핑, 런타임 상관 계획, 버전 비교 |
|
||||
| 브라우저 관찰 | 9 | origin 제한 CDP 캡처, bundle/source map 분석, WebMCP 검색, 세션 타임라인, capture diff 및 시각 증거 |
|
||||
| Electron 분석 | 5 | canonical 파일 루트 내 수동 관찰, 제한된 정적 앱 매핑, Evidence 기반 정적/런타임 조정 |
|
||||
| JavaScript 런타임 | 2 | 승인된 연결 전용 Node/Electron Inspector 대상 탐색과 평가·계측 없는 제한된 스크립트 및 실행 컨텍스트 관찰 |
|
||||
| 애플리케이션 | 10 | 제한된 교차 계층 추적, 고유 일치 버전 비교, 정적 export 반환 형태 비교, 승인된 Linux 격리 extracted-module replay, 관리형 런타임 특성화, 재구성 커버리지 폐쇄, 결정론적 의무 원장 |
|
||||
| 바이너리 세션 | 22 | `open_binary`, `binary_session`, 증거 번들, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
|
||||
|
||||
## 다른 코딩 에이전트에서 사용하기
|
||||
|
||||
@@ -173,7 +173,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@2.5.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-16
@@ -67,7 +67,7 @@ REA 让调查始终以二进制证据为依据。它不会声称能恢复原始
|
||||
### 使用编程智能体(推荐)
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
让智能体设置 REA。它会检查你的 Mac,说明需要安装的内容,先征得同意,并引导你处理系统提示。如果智能体要求重启以加载完整工具,请在设置后重启。
|
||||
@@ -80,7 +80,7 @@ npx rea-agents setup
|
||||
|
||||
`rea setup` 会先显示完整变更计划并请求确认。它不会安装或更新 Homebrew、Node.js 或 npm。缺少 [Hopper](https://www.hopperapp.com/) 时,Setup 会提议安装官方软件包;Hopper 是需要单独许可证的独立软件。
|
||||
|
||||
如果你已在 64 位 Linux 上安装 Ghidra 12.1.2 PUBLIC 和完整的 64 位 JDK 21,Setup 也可以在批准后登记 `GHIDRA_INSTALL_DIR` 和可选的 `JAVA_HOME`。REA 不会下载、安装或修改 Ghidra 或 Java。Ghidra provider 在隔离的只读 headless 会话中提供清单、反编译、汇编、调用关系、带类型的引用、xref、CFG 与函数 dossier;GUI 状态和修改操作仍不可用。
|
||||
如果你已在 64 位 Linux 上安装 Ghidra 12.1.4 PUBLIC 和完整的 64 位 JDK 21,Setup 也可以在批准后登记 `GHIDRA_INSTALL_DIR` 和可选的 `JAVA_HOME`。REA 不会下载、安装或修改 Ghidra 或 Java。Ghidra provider 在隔离的只读 headless 会话中提供清单、反编译、汇编、调用关系、带类型的引用、xref、CFG 与函数 dossier;GUI 状态和修改操作仍不可用。
|
||||
|
||||
#### Linux 安装与故障排除
|
||||
|
||||
@@ -91,7 +91,7 @@ npx rea-agents setup
|
||||
```bash
|
||||
# 1. 安装和配置 REA
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
如果 macOS 或安装程序要求确认,请完成提示,然后再次运行同一命令。
|
||||
@@ -149,18 +149,18 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
|
||||
|
||||
## 调查工具目录
|
||||
|
||||
| 工具类别 | 数量 | 示例 |
|
||||
| ----------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 二进制检查 | 36 | 过程、伪代码、汇编、字符串、名称、段、调用者、被调用者、交叉引用、注释 |
|
||||
| 组合分析 | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`、Swift 与 ObjC 发现 |
|
||||
| macOS 原生工具 | 5 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
|
||||
| 制品图谱 | 3 | 确定性目录、ZIP/APK/IPA 与 ASAR 清单,以及显式选择的事务式提取 |
|
||||
| Managed PE/CLI | 8 | PE/CLI 身份、元数据成员、CIL 哈希、P/Invoke/原生边界声明与验证、应用图谱投影、反编译重建导入、结构化 token 重映射、运行时关联计划与跨版本比较 |
|
||||
| 浏览器观察 | 9 | 精确来源 CDP 抓取、bundle/source map 分析、WebMCP 发现、会话时间线、capture diff 与视觉证据 |
|
||||
| Electron 分析 | 4 | 限定 canonical 文件根的被动观察、有界静态应用映射,以及基于 Evidence 的静态/运行时对账 |
|
||||
| JavaScript 运行时 | 2 | 经批准的只附加 Node/Electron Inspector 目标发现,以及不执行求值或插桩的有界脚本和执行上下文观察 |
|
||||
| 应用工作流 | 12 | 有界跨层追踪、唯一匹配跨版本图比较、静态导出返回形状比较、经批准的 Linux 隔离 extracted-module replay、托管运行时表征、重建覆盖闭合、确定性义务账本和端到端就绪性一致性验证 |
|
||||
| 二进制会话 | 19 | `open_binary`、`binary_session`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
|
||||
| 工具类别 | 数量 | 示例 |
|
||||
| ----------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 二进制检查 | 39 | 过程、伪代码、汇编、字符串、名称、段、调用者、被调用者、交叉引用、注释 |
|
||||
| 组合分析 | 15 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`、Swift 与 ObjC 发现 |
|
||||
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
|
||||
| 制品图谱 | 5 | 目录及支持的软件包内联检查、Interface Builder UI 图解、Apple 资源目录元数据分页检查,以及显式选择的事务式提取 |
|
||||
| Managed PE/CLI | 8 | PE/CLI 身份、元数据成员、CIL 哈希、P/Invoke/原生边界声明与验证、应用图谱投影、反编译重建导入、结构化 token 重映射、运行时关联计划与跨版本比较 |
|
||||
| 浏览器观察 | 9 | 精确来源 CDP 抓取、bundle/source map 分析、WebMCP 发现、会话时间线、capture diff 与视觉证据 |
|
||||
| Electron 分析 | 5 | 限定 canonical 文件根的被动观察、有界静态应用映射,以及基于 Evidence 的静态/运行时对账 |
|
||||
| JavaScript 运行时 | 2 | 经批准的只附加 Node/Electron Inspector 目标发现,以及不执行求值或插桩的有界脚本和执行上下文观察 |
|
||||
| 应用工作流 | 10 | 有界跨层追踪、唯一匹配跨版本图比较、静态导出返回形状比较、经批准的 Linux 隔离 extracted-module replay、托管运行时表征、重建覆盖闭合和确定性义务账本 |
|
||||
| 二进制会话 | 22 | `open_binary`、`binary_session`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
|
||||
|
||||
## 与其他编程智能体一起使用
|
||||
|
||||
@@ -173,7 +173,7 @@ Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Win
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@2.5.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1249
-740
File diff suppressed because it is too large
Load Diff
+190
-365
@@ -9,19 +9,15 @@ import json
|
||||
import hmac
|
||||
import os
|
||||
import re
|
||||
import sre_parse
|
||||
import socket
|
||||
from typing import Any, Optional, Protocol, Sequence
|
||||
|
||||
MAX_LINE_BYTES = 10 * 1024 * 1024
|
||||
MAX_READ_BYTES = 4096
|
||||
BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
|
||||
_selected_document = None
|
||||
_rea_session_document = None
|
||||
_search_inventory_cache = {}
|
||||
_pseudocode_cache = {}
|
||||
_hopper_api = None
|
||||
MAX_SEARCH_PATTERN_LENGTH = 256
|
||||
MAX_SEARCH_VALUE_LENGTH = 4096
|
||||
|
||||
|
||||
class HopperDocument(Protocol):
|
||||
@@ -69,8 +65,9 @@ class HopperApiFacade:
|
||||
|
||||
def _configure_hopper_api(document_provider):
|
||||
"""Inject Hopper's document provider or a standalone test facade."""
|
||||
global _hopper_api
|
||||
global _hopper_api, _rea_session_document
|
||||
_hopper_api = HopperApiFacade(document_provider)
|
||||
_rea_session_document = None
|
||||
|
||||
|
||||
def _api():
|
||||
@@ -88,25 +85,53 @@ def _api():
|
||||
|
||||
|
||||
def _session_document():
|
||||
"""Find only the document opened for this authenticated REA session."""
|
||||
"""Return the exact document bound to this authenticated REA session."""
|
||||
global _rea_session_document
|
||||
documents = _api().documents()
|
||||
if _rea_session_document is not None:
|
||||
return (
|
||||
_rea_session_document
|
||||
if any(document is _rea_session_document for document in documents)
|
||||
else None
|
||||
)
|
||||
return _bind_session_document(documents)
|
||||
|
||||
|
||||
def _bind_session_document(documents=None):
|
||||
"""Bind this bridge to Hopper's current document for its target."""
|
||||
global _rea_session_document
|
||||
api = _api()
|
||||
if documents is None:
|
||||
documents = api.documents()
|
||||
target = os.path.realpath(REA_TARGET_PATH)
|
||||
for document in _api().documents():
|
||||
|
||||
def matches_target(document):
|
||||
paths = (document.getExecutableFilePath(), document.getDatabaseFilePath())
|
||||
for path in paths:
|
||||
if path and os.path.realpath(path) == target:
|
||||
return document
|
||||
return any(path and os.path.realpath(path) == target for path in paths)
|
||||
|
||||
current = api.current_document()
|
||||
if current is not None and matches_target(current):
|
||||
_rea_session_document = current
|
||||
return current
|
||||
matching = [document for document in documents if matches_target(document)]
|
||||
if len(matching) == 1:
|
||||
_rea_session_document = matching[0]
|
||||
return matching[0]
|
||||
if len(matching) > 1:
|
||||
raise CapabilityUnavailableError(
|
||||
"The REA session document is ambiguous; Hopper did not identify the launched document"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
MAX_REGEX_BACKTRACKING_PATHS = 10000
|
||||
MAX_REGEX_CANDIDATE_LENGTH = 4096
|
||||
MAX_REGEX_SEARCH_WORK_UNITS = 1000000
|
||||
|
||||
|
||||
class CapabilityUnavailableError(Exception):
|
||||
"""The active Hopper build lacks one admitted public API operation."""
|
||||
|
||||
|
||||
class InvalidRequestError(Exception):
|
||||
"""Caller input failed bridge request or operation validation."""
|
||||
|
||||
|
||||
EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
|
||||
(
|
||||
"analyze_function",
|
||||
@@ -129,6 +154,11 @@ EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
|
||||
|
||||
|
||||
def _hex(value):
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
try:
|
||||
value = int(str(value), 16)
|
||||
except ValueError as error:
|
||||
raise ValueError("Hopper returned a non-hexadecimal address") from error
|
||||
return "0x%x" % value
|
||||
|
||||
|
||||
@@ -149,17 +179,25 @@ def _document(name=None):
|
||||
api = _api()
|
||||
documents = api.documents()
|
||||
if name is not None:
|
||||
for candidate in documents:
|
||||
if candidate.getDocumentName() == name:
|
||||
return candidate
|
||||
raise ValueError("Unknown Hopper document")
|
||||
matches = [candidate for candidate in documents if candidate.getDocumentName() == name]
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
if len(matches) > 1:
|
||||
current = api.current_document()
|
||||
if current is not None and current.getDocumentName() == name:
|
||||
return current
|
||||
raise InvalidRequestError("Hopper document name is ambiguous")
|
||||
raise InvalidRequestError("Unknown Hopper document")
|
||||
if _selected_document is not None:
|
||||
for candidate in documents:
|
||||
if candidate.getDocumentName() == _selected_document:
|
||||
return candidate
|
||||
session_document = _session_document()
|
||||
if session_document is not None:
|
||||
return session_document
|
||||
current = api.current_document()
|
||||
if current is None:
|
||||
raise ValueError("No Hopper document is loaded")
|
||||
raise CapabilityUnavailableError("No Hopper document is loaded")
|
||||
return current
|
||||
|
||||
|
||||
@@ -168,20 +206,20 @@ def _address(document, value=None):
|
||||
if value is None:
|
||||
return document.getCurrentAddress()
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("Address must be a string")
|
||||
raise InvalidRequestError("Address must be a string")
|
||||
try:
|
||||
return int(value, 16)
|
||||
except ValueError:
|
||||
result = document.getAddressForName(value)
|
||||
if result in BAD_ADDRESSES:
|
||||
raise ValueError("Unknown Hopper address or name")
|
||||
raise InvalidRequestError("Unknown Hopper address or name")
|
||||
return result
|
||||
|
||||
|
||||
def _segment(document, address):
|
||||
result = document.getSegmentAtAddress(address)
|
||||
if result is None:
|
||||
raise ValueError("Address is outside every segment")
|
||||
raise InvalidRequestError("Address is outside every segment")
|
||||
return result
|
||||
|
||||
|
||||
@@ -189,7 +227,7 @@ def _procedure(document, value=None):
|
||||
address = document.getCurrentAddress() if value is None else _address(document, value)
|
||||
result = _segment(document, address).getProcedureAtAddress(address)
|
||||
if result is None:
|
||||
raise ValueError("No procedure exists at the requested address")
|
||||
raise InvalidRequestError("No procedure exists at the requested address")
|
||||
return result
|
||||
|
||||
|
||||
@@ -225,18 +263,14 @@ def _containing_procedure(document, address):
|
||||
return (procedure, None) if procedure is not None else (None, "not_in_procedure")
|
||||
|
||||
|
||||
def _instruction_addresses(procedure, limit):
|
||||
def _instruction_addresses(procedure):
|
||||
result = []
|
||||
seen = set()
|
||||
segment = procedure.getSegment()
|
||||
truncated = False
|
||||
for block in procedure.basicBlockIterator():
|
||||
address = block.getStartingAddress()
|
||||
end = block.getEndingAddress()
|
||||
while address < end and address not in seen:
|
||||
if len(result) >= limit:
|
||||
truncated = True
|
||||
return result, truncated
|
||||
seen.add(address)
|
||||
instruction = segment.getInstructionAtAddress(address)
|
||||
if instruction is None:
|
||||
@@ -246,24 +280,15 @@ def _instruction_addresses(procedure, limit):
|
||||
if length <= 0:
|
||||
break
|
||||
address += length
|
||||
return result, truncated
|
||||
return result
|
||||
|
||||
|
||||
def _procedure_references(document, params):
|
||||
procedure = _procedure(document, params.get("procedure"))
|
||||
direction = params.get("direction", "outgoing")
|
||||
offset = params.get("offset", 0)
|
||||
limit = params.get("limit", 100)
|
||||
max_instructions = params.get("max_instructions", 500)
|
||||
if direction not in ("incoming", "outgoing"):
|
||||
raise ValueError("direction must be incoming or outgoing")
|
||||
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
|
||||
raise ValueError("offset must be a non-negative integer")
|
||||
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
|
||||
raise ValueError("limit must be an integer between 1 and 500")
|
||||
if not isinstance(max_instructions, int) or isinstance(max_instructions, bool) or max_instructions < 1 or max_instructions > 5000:
|
||||
raise ValueError("max_instructions must be an integer between 1 and 5000")
|
||||
addresses, scan_truncated = _instruction_addresses(procedure, max_instructions)
|
||||
raise InvalidRequestError("direction must be incoming or outgoing")
|
||||
addresses = _instruction_addresses(procedure)
|
||||
edges = set()
|
||||
for address in addresses:
|
||||
segment = _segment(document, address)
|
||||
@@ -272,8 +297,7 @@ def _procedure_references(document, params):
|
||||
edges.add((address, reference) if direction == "outgoing" else (reference, address))
|
||||
ordered = sorted(edges)
|
||||
items = []
|
||||
selected = ordered[offset:offset + limit]
|
||||
for source, target in selected:
|
||||
for source, target in ordered:
|
||||
source_procedure, _ = _containing_procedure(document, source)
|
||||
target_procedure, _ = _containing_procedure(document, target)
|
||||
items.append({
|
||||
@@ -283,13 +307,9 @@ def _procedure_references(document, params):
|
||||
"target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
|
||||
"kind": _unavailable("Hopper's public Python API does not classify reference kinds"),
|
||||
})
|
||||
next_offset = offset + len(selected)
|
||||
has_more = next_offset < len(ordered)
|
||||
truncated = scan_truncated or has_more
|
||||
return {
|
||||
"procedure": _procedure_identity(procedure), "direction": direction,
|
||||
"references": {"items": items, "total": None if scan_truncated else len(ordered), "returned": len(items), "truncated": truncated, "next_offset": next_offset if has_more and not scan_truncated else None},
|
||||
"instructions_scanned": len(addresses), "instruction_scan_truncated": scan_truncated,
|
||||
"references": items,
|
||||
}
|
||||
|
||||
|
||||
@@ -352,230 +372,39 @@ def _search_inventory(document, kind):
|
||||
return inventory
|
||||
|
||||
|
||||
def _checked_regex_paths(left, right, operation):
|
||||
"""Apply one path-count operation without crossing the static work budget."""
|
||||
if operation == "add":
|
||||
exceeded = left > MAX_REGEX_BACKTRACKING_PATHS - right
|
||||
result = left + right
|
||||
else:
|
||||
exceeded = right != 0 and left > MAX_REGEX_BACKTRACKING_PATHS // right
|
||||
result = left * right
|
||||
if exceeded or result > MAX_REGEX_BACKTRACKING_PATHS:
|
||||
raise ValueError(
|
||||
"Regex exceeds the %d-path backtracking budget"
|
||||
% MAX_REGEX_BACKTRACKING_PATHS
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _repeat_regex_paths(child_paths, minimum, maximum):
|
||||
"""Count every bounded repetition path, including alternative child paths."""
|
||||
paths = 0
|
||||
repeated_paths = 1
|
||||
for count in range(maximum + 1):
|
||||
if count >= minimum:
|
||||
paths = _checked_regex_paths(paths, repeated_paths, "add")
|
||||
if count < maximum:
|
||||
repeated_paths = _checked_regex_paths(
|
||||
repeated_paths, child_paths, "multiply"
|
||||
)
|
||||
return paths
|
||||
|
||||
|
||||
def _validate_regex_class(items):
|
||||
"""Accept only constant-time character-class operations."""
|
||||
allowed = {
|
||||
sre_parse.CATEGORY,
|
||||
sre_parse.LITERAL,
|
||||
sre_parse.NEGATE,
|
||||
sre_parse.RANGE,
|
||||
}
|
||||
if any(operation not in allowed for operation, _ in items):
|
||||
raise ValueError("Regex operation is not supported by the bounded matcher")
|
||||
|
||||
|
||||
def _validate_regex_node(node, inside_repeat=False):
|
||||
"""Return capped path and step bounds for Python regex evaluation."""
|
||||
leaf_operations = {
|
||||
sre_parse.ANY,
|
||||
sre_parse.AT,
|
||||
sre_parse.CATEGORY,
|
||||
sre_parse.LITERAL,
|
||||
sre_parse.NOT_LITERAL,
|
||||
}
|
||||
forbidden = {
|
||||
sre_parse.ASSERT,
|
||||
sre_parse.ASSERT_NOT,
|
||||
sre_parse.GROUPREF,
|
||||
sre_parse.GROUPREF_EXISTS,
|
||||
}
|
||||
for name in ("GROUPREF_IGNORE", "GROUPREF_LOC_IGNORE", "GROUPREF_UNI_IGNORE"):
|
||||
operation = getattr(sre_parse, name, None)
|
||||
if operation is not None:
|
||||
forbidden.add(operation)
|
||||
repeat_tokens = {sre_parse.MAX_REPEAT, sre_parse.MIN_REPEAT}
|
||||
possessive = getattr(sre_parse, "POSSESSIVE_REPEAT", None)
|
||||
if possessive is not None:
|
||||
repeat_tokens.add(possessive)
|
||||
atomic = getattr(sre_parse, "ATOMIC_GROUP", None)
|
||||
|
||||
paths = 1
|
||||
steps = 0
|
||||
for operation, argument in node:
|
||||
if operation in forbidden:
|
||||
raise ValueError("Regex lookarounds and backreferences are not supported")
|
||||
if operation in leaf_operations:
|
||||
operation_paths = 1
|
||||
operation_steps = 1
|
||||
elif operation == sre_parse.IN:
|
||||
_validate_regex_class(argument)
|
||||
operation_paths = 1
|
||||
operation_steps = 1
|
||||
elif operation in repeat_tokens:
|
||||
if inside_repeat:
|
||||
raise ValueError("Nested regex repetitions are not supported")
|
||||
minimum, maximum, child = argument
|
||||
if maximum == sre_parse.MAXREPEAT or maximum > 1000:
|
||||
raise ValueError(
|
||||
"Unbounded or excessive regex repetitions are not supported"
|
||||
)
|
||||
child_paths, child_steps = _validate_regex_node(child, True)
|
||||
operation_paths = _repeat_regex_paths(
|
||||
child_paths, minimum, maximum
|
||||
)
|
||||
operation_steps = maximum * child_steps
|
||||
elif operation == sre_parse.SUBPATTERN:
|
||||
operation_paths, operation_steps = _validate_regex_node(
|
||||
argument[-1], inside_repeat
|
||||
)
|
||||
elif operation == sre_parse.BRANCH:
|
||||
operation_paths = 0
|
||||
operation_steps = 0
|
||||
for branch in argument[1]:
|
||||
branch_paths, branch_steps = _validate_regex_node(
|
||||
branch, inside_repeat
|
||||
)
|
||||
operation_paths = _checked_regex_paths(
|
||||
operation_paths,
|
||||
branch_paths,
|
||||
"add",
|
||||
)
|
||||
operation_steps = max(operation_steps, branch_steps)
|
||||
elif atomic is not None and operation == atomic:
|
||||
operation_paths, operation_steps = _validate_regex_node(
|
||||
argument, inside_repeat
|
||||
)
|
||||
else:
|
||||
raise ValueError("Regex operation is not supported by the bounded matcher")
|
||||
paths = _checked_regex_paths(paths, operation_paths, "multiply")
|
||||
steps += operation_steps
|
||||
return paths, steps
|
||||
|
||||
|
||||
def _bounded_regex_matcher(expression, backtracking_paths, steps_per_path):
|
||||
"""Create a matcher with per-candidate and cumulative work bounds."""
|
||||
remaining_work = MAX_REGEX_SEARCH_WORK_UNITS
|
||||
work_per_character = backtracking_paths * max(steps_per_path, 1)
|
||||
|
||||
def matches(value):
|
||||
nonlocal remaining_work
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("Regex candidates must be strings")
|
||||
if len(value) > MAX_REGEX_CANDIDATE_LENGTH:
|
||||
raise ValueError(
|
||||
"Regex candidate exceeds the %d-character safety limit"
|
||||
% MAX_REGEX_CANDIDATE_LENGTH
|
||||
)
|
||||
required_work = work_per_character * max(len(value), 1)
|
||||
if required_work > remaining_work:
|
||||
raise ValueError(
|
||||
"Regex search exceeds the %d-unit work budget"
|
||||
% MAX_REGEX_SEARCH_WORK_UNITS
|
||||
)
|
||||
remaining_work -= required_work
|
||||
return expression.search(value) is not None
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def _search_page(document, kind, params):
|
||||
def _search_results(document, kind, params):
|
||||
pattern = params.get("pattern")
|
||||
if not isinstance(pattern, str) or not pattern or len(pattern) > MAX_SEARCH_PATTERN_LENGTH:
|
||||
raise ValueError("pattern must contain between 1 and 256 characters")
|
||||
if not isinstance(pattern, str) or not pattern:
|
||||
raise InvalidRequestError("pattern must be a non-empty string")
|
||||
mode = params.get("mode", "literal")
|
||||
if mode not in ("literal", "regex"):
|
||||
raise ValueError("mode must be literal or regex")
|
||||
raise InvalidRequestError("mode must be literal or regex")
|
||||
case_sensitive = params.get("case_sensitive", False)
|
||||
if not isinstance(case_sensitive, bool):
|
||||
raise ValueError("case_sensitive must be a boolean")
|
||||
offset = params.get("offset", 0)
|
||||
limit = params.get("limit", 100)
|
||||
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
|
||||
raise ValueError("offset must be a non-negative integer")
|
||||
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 100:
|
||||
raise ValueError("limit must be an integer between 1 and 100")
|
||||
raise InvalidRequestError("case_sensitive must be a boolean")
|
||||
|
||||
if mode == "literal":
|
||||
needle = pattern if case_sensitive else pattern.casefold()
|
||||
matches = lambda value: needle in (value if case_sensitive else value.casefold())
|
||||
else:
|
||||
try:
|
||||
parsed = sre_parse.parse(pattern)
|
||||
backtracking_paths, steps_per_path = _validate_regex_node(parsed)
|
||||
expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
|
||||
except (re.error, OverflowError) as error:
|
||||
raise ValueError("Invalid regex pattern") from error
|
||||
matches = _bounded_regex_matcher(
|
||||
expression, backtracking_paths, steps_per_path
|
||||
)
|
||||
raise InvalidRequestError("Invalid regex pattern") from error
|
||||
matches = lambda value: expression.search(value) is not None
|
||||
|
||||
selected = []
|
||||
total = 0
|
||||
page_end = offset + limit
|
||||
for item in _search_inventory(document, kind):
|
||||
if not matches(item[1]):
|
||||
continue
|
||||
if offset <= total < page_end:
|
||||
selected.append(item)
|
||||
total += 1
|
||||
next_offset = offset + len(selected)
|
||||
has_more = next_offset < total
|
||||
return {
|
||||
"items": [
|
||||
selected.append(item)
|
||||
return [
|
||||
{
|
||||
"address": address,
|
||||
"value": value[:MAX_SEARCH_VALUE_LENGTH],
|
||||
"value_truncated": len(value) > MAX_SEARCH_VALUE_LENGTH,
|
||||
"value": value,
|
||||
}
|
||||
for address, value in selected
|
||||
],
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"next_offset": next_offset if has_more else None,
|
||||
"has_more": has_more,
|
||||
}
|
||||
|
||||
|
||||
def _page(values, offset, limit):
|
||||
"""Return one deterministically address-sorted page without crossing an unbounded map."""
|
||||
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
|
||||
raise ValueError("offset must be a non-negative integer")
|
||||
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
|
||||
raise ValueError("limit must be an integer between 1 and 500")
|
||||
ordered = sorted(values.items(), key=lambda item: int(item[0], 16))
|
||||
selected = ordered[offset:offset + limit]
|
||||
total = len(ordered)
|
||||
next_offset = offset + len(selected)
|
||||
has_more = next_offset < total
|
||||
return {
|
||||
"items": [{"address": address, "value": value} for address, value in selected],
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"next_offset": next_offset if has_more else None,
|
||||
"has_more": has_more,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def _unavailable(reason):
|
||||
@@ -583,36 +412,6 @@ def _unavailable(reason):
|
||||
return {"available": False, "reason": reason}
|
||||
|
||||
|
||||
def _offset(params, name):
|
||||
value = params.get(name, 0)
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise ValueError("%s must be a non-negative integer" % name)
|
||||
return value
|
||||
|
||||
|
||||
def _collection_offset(params, name):
|
||||
values = params.get("collection_offset", {})
|
||||
if not isinstance(values, dict):
|
||||
raise ValueError("collection_offset must be an object")
|
||||
value = values.get(name, 0)
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise ValueError("collection_offset.%s must be a non-negative integer" % name)
|
||||
return value
|
||||
|
||||
|
||||
def _bounded(items, offset, limit, total=None, scan_truncated=False):
|
||||
selected = items[offset:offset + limit]
|
||||
known_total = len(items) if total is None and not scan_truncated else total
|
||||
has_more = offset + len(selected) < len(items)
|
||||
return {
|
||||
"items": selected,
|
||||
"total": known_total,
|
||||
"returned": len(selected),
|
||||
"truncated": scan_truncated or has_more,
|
||||
"next_offset": offset + len(selected) if has_more else None,
|
||||
}
|
||||
|
||||
|
||||
def _name_map(document):
|
||||
result = {}
|
||||
for segment in document.getSegmentsList():
|
||||
@@ -639,8 +438,8 @@ def _render_instruction(segment, address):
|
||||
)
|
||||
|
||||
|
||||
def _assembly(procedure, limit=None):
|
||||
"""Render bounded assembly while guarding against malformed instruction cycles."""
|
||||
def _assembly(procedure):
|
||||
"""Render assembly while guarding against malformed instruction cycles."""
|
||||
lines = []
|
||||
segment = procedure.getSegment()
|
||||
seen = set()
|
||||
@@ -655,8 +454,6 @@ def _assembly(procedure, limit=None):
|
||||
line = _render_instruction(segment, address)
|
||||
if line is not None:
|
||||
lines.append(line)
|
||||
if limit is not None and len(lines) >= limit:
|
||||
return "\n".join(lines)
|
||||
length = instruction.getInstructionLength()
|
||||
if length <= 0:
|
||||
break
|
||||
@@ -665,37 +462,18 @@ def _assembly(procedure, limit=None):
|
||||
|
||||
|
||||
def _read_function_instructions(document, params):
|
||||
"""Read one bounded raw-instruction window without invoking decompilation."""
|
||||
"""Read all raw instructions without invoking decompilation."""
|
||||
procedure = _procedure(document, params.get("procedure"))
|
||||
offset = _offset(params, "offset")
|
||||
limit = params.get("limit", 64)
|
||||
if offset > 100000:
|
||||
raise ValueError("offset must not exceed 100000")
|
||||
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
|
||||
raise ValueError("limit must be an integer between 1 and 500")
|
||||
addresses, scan_truncated = _instruction_addresses(
|
||||
procedure, offset + limit + 1
|
||||
)
|
||||
start = min(offset, len(addresses))
|
||||
end = min(len(addresses), start + limit)
|
||||
addresses = _instruction_addresses(procedure)
|
||||
segment = procedure.getSegment()
|
||||
items = []
|
||||
for address in addresses[start:end]:
|
||||
for address in addresses:
|
||||
line = _render_instruction(segment, address)
|
||||
if line is not None:
|
||||
items.append(line)
|
||||
has_more = scan_truncated or end < len(addresses)
|
||||
return {
|
||||
"procedure": _procedure_identity(procedure),
|
||||
"instructions": {
|
||||
"items": items,
|
||||
"total": None if scan_truncated else len(addresses),
|
||||
"returned": len(items),
|
||||
"truncated": has_more,
|
||||
"next_offset": end if has_more else None,
|
||||
},
|
||||
"instructions_scanned": len(addresses),
|
||||
"instruction_scan_truncated": scan_truncated,
|
||||
"instructions": items,
|
||||
"limitations": [
|
||||
"Instruction text and ordering are Hopper-specific representations.",
|
||||
"The fast path does not decompile the procedure or scan whole-program names and strings.",
|
||||
@@ -704,20 +482,9 @@ def _read_function_instructions(document, params):
|
||||
|
||||
|
||||
def _analyze_function(document, params):
|
||||
"""Collect a bounded, single-pass function dossier for agent callers."""
|
||||
"""Collect the complete function dossier for agent callers."""
|
||||
procedure = _procedure(document, params.get("procedure"))
|
||||
limit = params.get("limit", 100)
|
||||
max_chars = params.get("max_pseudocode_chars", 20000)
|
||||
max_instructions = params.get("max_instructions", 500)
|
||||
pseudocode_offset = _offset(params, "pseudocode_offset")
|
||||
assembly_offset = _offset(params, "assembly_offset")
|
||||
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
|
||||
raise ValueError("limit must be an integer between 1 and 500")
|
||||
if not isinstance(max_chars, int) or isinstance(max_chars, bool) or max_chars < 1 or max_chars > 100000:
|
||||
raise ValueError("max_pseudocode_chars must be between 1 and 100000")
|
||||
if not isinstance(max_instructions, int) or isinstance(max_instructions, bool) or max_instructions < 1 or max_instructions > 5000:
|
||||
raise ValueError("max_instructions must be between 1 and 5000")
|
||||
addresses, instruction_scan_truncated = _instruction_addresses(procedure, max_instructions)
|
||||
addresses = _instruction_addresses(procedure)
|
||||
blocks = []
|
||||
all_blocks = list(procedure.basicBlockIterator())
|
||||
for block in all_blocks:
|
||||
@@ -732,7 +499,7 @@ def _analyze_function(document, params):
|
||||
"successors": sorted(set(successors), key=lambda value: int(value, 16)),
|
||||
})
|
||||
pseudo = _pseudocode(document, procedure) or ""
|
||||
assembly_lines = _assembly(procedure, max_instructions).splitlines() if params.get("include_assembly", False) else []
|
||||
assembly_lines = _assembly(procedure).splitlines()
|
||||
callers = sorted((_procedure_identity(item) for item in procedure.getAllCallerProcedures()), key=lambda item: int(item["address"], 16))
|
||||
callees = sorted((_procedure_identity(item) for item in procedure.getAllCalleeProcedures()), key=lambda item: int(item["address"], 16))
|
||||
comments = []
|
||||
@@ -785,28 +552,17 @@ def _analyze_function(document, params):
|
||||
comments.sort(key=lambda item: (int(item["address"], 16), item["kind"]))
|
||||
referenced_strings.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
|
||||
referenced_names.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
|
||||
pseudo_text = pseudo[pseudocode_offset:pseudocode_offset + max_chars]
|
||||
pseudo_next = pseudocode_offset + len(pseudo_text)
|
||||
def collection(name, items, scan_limited=False):
|
||||
return _bounded(items, _collection_offset(params, name), limit, None, scan_limited)
|
||||
return {
|
||||
"procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "classification": None, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
|
||||
"pseudocode": {"text": pseudo_text, "total_chars": len(pseudo), "returned_chars": len(pseudo_text), "truncated": pseudo_next < len(pseudo), "next_offset": pseudo_next if pseudo_next < len(pseudo) else None},
|
||||
"assembly": _bounded(
|
||||
assembly_lines,
|
||||
assembly_offset,
|
||||
max_instructions,
|
||||
None,
|
||||
instruction_scan_truncated if params.get("include_assembly", False) else False,
|
||||
),
|
||||
"comments": collection("comments", comments, instruction_scan_truncated),
|
||||
"callers": collection("callers", callers), "callees": collection("callees", callees),
|
||||
"incoming_references": collection("incoming_references", incoming, instruction_scan_truncated),
|
||||
"outgoing_references": collection("outgoing_references", outgoing, instruction_scan_truncated),
|
||||
"referenced_strings": collection("referenced_strings", referenced_strings, instruction_scan_truncated),
|
||||
"referenced_names": collection("referenced_names", referenced_names, instruction_scan_truncated),
|
||||
"basic_blocks": collection("basic_blocks", blocks),
|
||||
"instruction_scan": {"scanned": len(addresses), "truncated": instruction_scan_truncated},
|
||||
"pseudocode": pseudo,
|
||||
"assembly": assembly_lines,
|
||||
"comments": comments,
|
||||
"callers": callers, "callees": callees,
|
||||
"incoming_references": incoming,
|
||||
"outgoing_references": outgoing,
|
||||
"referenced_strings": referenced_strings,
|
||||
"referenced_names": referenced_names,
|
||||
"basic_blocks": blocks,
|
||||
"limitations": [
|
||||
"Hopper's public Python API does not classify reference kinds.",
|
||||
"Hopper's public Python API does not expose equivalent external or thunk classification in this dossier.",
|
||||
@@ -825,6 +581,13 @@ def _dispatch(method, params):
|
||||
document = _session_document()
|
||||
if document is None:
|
||||
return {"shutdown": True, "analysis_stopped": True, "document_closed": True}
|
||||
if method == "shutdown" and not REA_OWNS_PROCESS_LIFETIME:
|
||||
return {
|
||||
"shutdown": True,
|
||||
"analysis_stopped": not document.backgroundProcessActive(),
|
||||
"document_closed": False,
|
||||
"document_retained": True,
|
||||
}
|
||||
if document.backgroundProcessActive():
|
||||
document.requestBackgroundProcessStop()
|
||||
if method == "shutdown" and REA_OWNS_PROCESS_LIFETIME:
|
||||
@@ -864,9 +627,9 @@ def _dispatch(method, params):
|
||||
if method == "read_bytes":
|
||||
length = params.get("length", 256)
|
||||
if isinstance(length, bool) or not isinstance(length, int):
|
||||
raise ValueError("Byte-read length must be an integer")
|
||||
if length < 1 or length > MAX_READ_BYTES:
|
||||
raise ValueError("Byte-read length must be between 1 and 4096")
|
||||
raise InvalidRequestError("Byte-read length must be an integer")
|
||||
if length < 1:
|
||||
raise InvalidRequestError("Byte-read length must be at least 1")
|
||||
reader = getattr(document, "readBytes", None)
|
||||
if not callable(reader):
|
||||
raise CapabilityUnavailableError(
|
||||
@@ -900,7 +663,7 @@ def _dispatch(method, params):
|
||||
or offset in BAD_ADDRESSES
|
||||
or offset < 0
|
||||
):
|
||||
raise ValueError("Address has no authoritative file-offset mapping")
|
||||
raise InvalidRequestError("Address has no authoritative file-offset mapping")
|
||||
return {"address": _hex(address), "file_offset": offset}
|
||||
if method == "resolve_containing_procedure":
|
||||
address = _address(document, params.get("address"))
|
||||
@@ -936,7 +699,7 @@ def _dispatch(method, params):
|
||||
else:
|
||||
result = document.getInstructionStart(max(0, target - 1))
|
||||
if result in BAD_ADDRESSES:
|
||||
raise ValueError("No adjacent address")
|
||||
raise InvalidRequestError("No adjacent address")
|
||||
return _hex(result)
|
||||
if method == "list_segments":
|
||||
result = []
|
||||
@@ -968,24 +731,24 @@ def _dispatch(method, params):
|
||||
})
|
||||
return result
|
||||
if method == "list_procedures":
|
||||
return _page(dict(_search_inventory(document, "procedure")), params.get("offset", 0), params.get("limit", 100))
|
||||
return [{"address": address, "value": value} for address, value in _search_inventory(document, "procedure")]
|
||||
if method == "list_strings":
|
||||
values = dict(_search_inventory(document, "string"))
|
||||
requested = params.get("address")
|
||||
if requested is not None:
|
||||
key = _hex(_address(document, requested))
|
||||
values = {key: values[key]} if key in values else {}
|
||||
return _page(values, params.get("offset", 0), params.get("limit", 100))
|
||||
return [{"address": address, "value": value} for address, value in values.items()]
|
||||
if method == "list_names":
|
||||
result = dict(_search_inventory(document, "name"))
|
||||
requested = params.get("address")
|
||||
if requested is not None:
|
||||
key = _hex(_address(document, requested))
|
||||
result = {key: result[key]} if key in result else {}
|
||||
return _page(result, params.get("offset", 0), params.get("limit", 100))
|
||||
return [{"address": address, "value": value} for address, value in result.items()]
|
||||
if method in ("search_procedures", "search_strings"):
|
||||
kind = "procedure" if method == "search_procedures" else "string"
|
||||
return _search_page(document, kind, params)
|
||||
return _search_results(document, kind, params)
|
||||
if method.startswith("procedure_"):
|
||||
procedure = _procedure(document, params.get("procedure"))
|
||||
if method == "procedure_address":
|
||||
@@ -1035,50 +798,111 @@ def _dispatch(method, params):
|
||||
address = _address(document, params.get("address"))
|
||||
document.removeBookmarkAtAddress(address)
|
||||
return not document.hasBookmarkAtAddress(address)
|
||||
raise ValueError("Unknown bridge method")
|
||||
raise InvalidRequestError("Unknown bridge method")
|
||||
|
||||
|
||||
def _serve_connection(connection):
|
||||
"""Serve one size-bounded, capability-authenticated NDJSON connection."""
|
||||
"""Serve one capability-authenticated NDJSON connection."""
|
||||
file = connection.makefile("rwb")
|
||||
while True:
|
||||
line = file.readline(MAX_LINE_BYTES + 1)
|
||||
if not line or len(line) > MAX_LINE_BYTES:
|
||||
line = file.readline()
|
||||
if not line:
|
||||
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")
|
||||
try:
|
||||
request = json.loads(line.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||
raise InvalidRequestError("Invalid bridge request JSON") from error
|
||||
if not isinstance(request, dict) or set(request) != {"id", "token", "method", "params"}:
|
||||
raise InvalidRequestError("Invalid bridge request shape")
|
||||
request_id = request["id"]
|
||||
if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
|
||||
if isinstance(request_id, bool) or not isinstance(request_id, int) or request_id < 0:
|
||||
raise InvalidRequestError("Invalid bridge request id")
|
||||
if (
|
||||
not isinstance(request["method"], str) or not request["method"]
|
||||
or not isinstance(request["params"], dict)
|
||||
):
|
||||
raise InvalidRequestError("Invalid bridge method or parameters")
|
||||
if (
|
||||
not isinstance(request["token"], str) or not request["token"].isascii()
|
||||
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"
|
||||
if isinstance(error, PermissionError):
|
||||
return "authorization"
|
||||
if isinstance(error, (ValueError, TypeError, KeyError)):
|
||||
if isinstance(error, InvalidRequestError):
|
||||
return "invalid_request"
|
||||
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)
|
||||
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__
|
||||
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):
|
||||
@@ -1097,6 +921,7 @@ def _run():
|
||||
|
||||
|
||||
# Hopper's public objects are bound to its dedicated Python execution thread.
|
||||
# Keep dispatch on that thread; moving calls to an arbitrary worker can deadlock.
|
||||
# Keep dispatch on that thread; moving calls to a worker can deadlock.
|
||||
if __name__ == "__main__":
|
||||
_bind_session_document()
|
||||
_run()
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import AppKit
|
||||
import ApplicationServices
|
||||
import CryptoKit
|
||||
import ScreenCaptureKit
|
||||
import Darwin
|
||||
|
||||
struct Request: Decodable {
|
||||
var pid: Int32
|
||||
var window_id: UInt32
|
||||
var executable: String
|
||||
var sha256: String
|
||||
var launch_time: Double?
|
||||
var screenshot: Bool
|
||||
var accessibility: Bool
|
||||
var max_nodes: Int
|
||||
var action: Action?
|
||||
}
|
||||
struct Action: Decodable { var kind: String; var path: [Int]?; var direction: String?; var text: String? }
|
||||
struct BoundaryFailure: Error { var code: String; var message: String }
|
||||
func fail(_ code: String, _ message: String) throws -> Never { throw BoundaryFailure(code: code, message: message) }
|
||||
func attribute(_ element: AXUIElement, _ key: String) -> CFTypeRef? {
|
||||
var value: CFTypeRef?
|
||||
return AXUIElementCopyAttributeValue(element, key as CFString, &value) == .success ? value : nil
|
||||
}
|
||||
func childCount(_ element: AXUIElement) -> Int {
|
||||
var count = 0
|
||||
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
|
||||
}
|
||||
func children(_ element: AXUIElement, limit: Int = 2000) -> [AXUIElement] {
|
||||
var value: CFArray?
|
||||
let count = min(limit, childCount(element))
|
||||
if count == 0 { return [] }
|
||||
return AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success ? value as? [AXUIElement] ?? [] : []
|
||||
}
|
||||
func text(_ element: AXUIElement, _ key: String) -> Any { (attribute(element, key) as? String).map { String($0.prefix(4096)) } ?? NSNull() as Any }
|
||||
func windowBounds(_ element: AXUIElement) -> CGRect? {
|
||||
guard let p = attribute(element, kAXPositionAttribute), let s = attribute(element, kAXSizeAttribute),
|
||||
CFGetTypeID(p) == AXValueGetTypeID(), CFGetTypeID(s) == AXValueGetTypeID() else { return nil }
|
||||
var point = CGPoint.zero; var size = CGSize.zero
|
||||
guard AXValueGetValue(p as! AXValue, .cgPoint, &point), AXValueGetValue(s as! AXValue, .cgSize, &size) else { return nil }
|
||||
return CGRect(origin: point, size: size)
|
||||
}
|
||||
func observe(_ request: Request) async throws -> [String: Any] {
|
||||
guard let app = NSRunningApplication(processIdentifier: request.pid), let url = app.executableURL else {
|
||||
try fail("target-mismatch", "Selected PID does not expose an application executable URL")
|
||||
}
|
||||
guard let canonicalPointer = realpath(url.path, nil) else { try fail("target-mismatch", "Running executable path cannot be resolved") }
|
||||
defer { free(canonicalPointer) }
|
||||
let canonicalExecutable = String(cString: canonicalPointer)
|
||||
guard canonicalExecutable == request.executable else {
|
||||
try fail("target-mismatch", "Selected PID executable \(canonicalExecutable) differs from active target \(request.executable)")
|
||||
}
|
||||
guard let launch = app.launchDate?.timeIntervalSince1970 else {
|
||||
try fail("process-identity-unavailable", "Selected application does not expose a launch time; stable PID reuse checks are unavailable")
|
||||
}
|
||||
if let expected = request.launch_time, expected != launch { try fail("process-replaced", "Selected process was replaced") }
|
||||
let executableSize = (try FileManager.default.attributesOfItem(atPath: url.path)[.size] as? NSNumber)?.int64Value ?? -1
|
||||
guard executableSize >= 0 && executableSize <= 256 * 1024 * 1024 else { try fail("target-limit", "Running executable exceeds the 256 MiB identity-check budget") }
|
||||
let executableBytes = try Data(contentsOf: url, options: .mappedIfSafe)
|
||||
guard SHA256.hash(data: executableBytes).map({ String(format: "%02x", $0) }).joined() == request.sha256 else {
|
||||
try fail("target-mismatch", "Running executable bytes differ from the active target digest")
|
||||
}
|
||||
guard let windows = CGWindowListCopyWindowInfo([.optionIncludingWindow], request.window_id) as? [[String: Any]],
|
||||
windows.count == 1, let window = windows.first,
|
||||
(window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == request.pid,
|
||||
(window[kCGWindowLayer as String] as? NSNumber)?.intValue == 0,
|
||||
let boundsDictionary = window[kCGWindowBounds as String] as? [String: Any],
|
||||
let bounds = CGRect(dictionaryRepresentation: boundsDictionary as CFDictionary) else {
|
||||
try fail("window-mismatch", "Selected window is absent or belongs to another process")
|
||||
}
|
||||
if request.screenshot && !CGPreflightScreenCaptureAccess() {
|
||||
try fail("screen-recording-denied", "Grant Screen Recording to the REA host in System Settings > Privacy & Security, then retry; no broad capture fallback is used")
|
||||
}
|
||||
var selected: AXUIElement?
|
||||
if request.accessibility || request.action != nil {
|
||||
guard AXIsProcessTrusted() else { try fail("accessibility-denied", "Grant Accessibility to the REA host in System Settings > Privacy & Security, then retry") }
|
||||
let application = AXUIElementCreateApplication(request.pid)
|
||||
AXUIElementSetMessagingTimeout(application, 2)
|
||||
guard let axWindows = attribute(application, kAXWindowsAttribute) as? [AXUIElement] else {
|
||||
try fail("accessibility-unavailable", "Selected application does not expose accessibility windows")
|
||||
}
|
||||
let matches = axWindows.filter { windowBounds($0) == bounds }
|
||||
guard matches.count == 1 else { try fail("ambiguous-window", "Accessibility window geometry does not identify exactly one selected window; selected CG bounds \(bounds), AX candidates \(axWindows.map { String(describing: windowBounds($0)) })") }
|
||||
selected = matches[0]
|
||||
}
|
||||
if let action = request.action, let root = selected {
|
||||
var element = root
|
||||
for index in action.path ?? [] {
|
||||
var selectedChild: CFArray?
|
||||
guard index >= 0 && index < childCount(element),
|
||||
AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, index, 1, &selectedChild) == .success,
|
||||
let item = (selectedChild as? [AXUIElement])?.first else { try fail("element-missing", "Selected accessibility path no longer exists") }
|
||||
element = item
|
||||
}
|
||||
var owner: pid_t = 0
|
||||
guard AXUIElementGetPid(element, &owner) == .success && owner == request.pid else { try fail("element-owner-mismatch", "Selected element belongs to another process") }
|
||||
let outcome: AXError
|
||||
switch action.kind {
|
||||
case "click": outcome = AXUIElementPerformAction(element, kAXPressAction as CFString)
|
||||
case "scroll": outcome = AXUIElementPerformAction(element, (action.direction == "increment" ? kAXIncrementAction : kAXDecrementAction) as CFString)
|
||||
case "key-entry": outcome = AXUIElementSetAttributeValue(element, kAXValueAttribute as CFString, (action.text ?? "") as CFString)
|
||||
default: try fail("unsupported-action", "Only selected-element press, increment/decrement and text-value entry are admitted")
|
||||
}
|
||||
guard outcome == .success else { try fail("action-failed", "Accessibility action failed with AXError \(outcome.rawValue)") }
|
||||
}
|
||||
var nodes: [[String: Any]] = []; var truncated = false
|
||||
if request.accessibility, let root = selected {
|
||||
var pending: [(AXUIElement, [Int])] = [(root, [])]
|
||||
while let (element, path) = pending.popLast() {
|
||||
if nodes.count >= request.max_nodes { truncated = true; break }
|
||||
let items = children(element)
|
||||
var actions: CFArray?
|
||||
AXUIElementCopyActionNames(element, &actions)
|
||||
nodes.append(["path": path, "role": text(element, kAXRoleAttribute), "title": text(element, kAXTitleAttribute), "value": text(element, kAXValueAttribute), "actions": actions as? [String] ?? [], "children_count": childCount(element)])
|
||||
if path.count >= 32 { if !items.isEmpty { truncated = true }; continue }
|
||||
let available = max(0, request.max_nodes - nodes.count - pending.count)
|
||||
let count = min(available, items.count)
|
||||
if childCount(element) > count { truncated = true }
|
||||
for index in (0..<count).reversed() { pending.append((items[index], path + [index])) }
|
||||
}
|
||||
}
|
||||
var screenshot: Any = NSNull()
|
||||
if request.screenshot {
|
||||
guard #available(macOS 14, *) else { try fail("unsupported-host", "Selected-window screenshots require macOS 14 or later") }
|
||||
let content = try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: false)
|
||||
guard let scWindow = content.windows.first(where: { $0.windowID == request.window_id && $0.owningApplication?.processID == request.pid }) else {
|
||||
try fail("capture-window-missing", "Selected window disappeared before capture")
|
||||
}
|
||||
guard bounds.width > 0 && bounds.height > 0 && bounds.width <= 100000 && bounds.height <= 100000 else { try fail("window-bounds", "Selected window has invalid or oversized geometry") }
|
||||
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
|
||||
let configuration = SCStreamConfiguration()
|
||||
let scale = min(1, 2048 / max(bounds.width, bounds.height))
|
||||
configuration.width = max(1, Int(bounds.width * scale)); configuration.height = max(1, Int(bounds.height * scale))
|
||||
configuration.showsCursor = false
|
||||
let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
|
||||
guard let png = NSBitmapImageRep(cgImage: image).representation(using: .png, properties: [:]), png.count <= 8 * 1024 * 1024 else {
|
||||
try fail("image-limit", "Selected-window PNG exceeded 8 MiB")
|
||||
}
|
||||
screenshot = ["mime_type": "image/png", "base64": png.base64EncodedString(), "sha256": SHA256.hash(data: png).map({ String(format: "%02x", $0) }).joined(), "width": image.width, "height": image.height]
|
||||
}
|
||||
return ["window": ["pid": request.pid, "window_id": request.window_id, "executable": request.executable, "launch_time": launch, "title": window[kCGWindowName as String] as? String ?? ""], "nodes": nodes, "truncated": truncated, "screenshot": screenshot, "gaps": request.accessibility ? [] : ["Accessibility capture was disabled"]]
|
||||
}
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let request = try JSONDecoder().decode(Request.self, from: Data(CommandLine.arguments[1].utf8))
|
||||
let result = try await observe(request)
|
||||
let output = try JSONSerialization.data(withJSONObject: ["ok": true, "result": result], options: [.sortedKeys])
|
||||
FileHandle.standardOutput.write(output)
|
||||
} catch {
|
||||
let failure = error as? BoundaryFailure
|
||||
let output: [String: Any] = ["ok": false, "code": failure?.code ?? "capture-failed", "message": failure?.message ?? error.localizedDescription]
|
||||
if let data = try? JSONSerialization.data(withJSONObject: output, options: [.sortedKeys]) { FileHandle.standardOutput.write(data) }
|
||||
}
|
||||
exit(0)
|
||||
}
|
||||
RunLoop.main.run()
|
||||
@@ -278,9 +278,7 @@ The adapter returns this provider-neutral envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"provider": { "id": "ghidra", "version": "<concrete version>" },
|
||||
"provider_profile_schema_version": 1,
|
||||
"parameters": {},
|
||||
"digest": "<sha256 of RFC 8785 canonical JSON excluding digest>"
|
||||
}
|
||||
@@ -295,8 +293,8 @@ parameters as opaque data.
|
||||
A selected deep binding requires a concrete provider version. If the adapter
|
||||
cannot determine it, the candidate is unavailable with `version_unresolved`;
|
||||
REA does not persist a cache that merely commits `null` or "whatever is
|
||||
installed". Provider identity, provider-profile schema version, normalized
|
||||
parameters, or provider version changes produce a different digest.
|
||||
installed". Provider identity, normalized parameters, or provider version
|
||||
changes produce a different digest.
|
||||
|
||||
The same target, configuration, and provider must resolve the same profile over
|
||||
CLI and MCP. Profile resolution must be side-effect-free and complete before
|
||||
@@ -304,11 +302,10 @@ the provider imports the target.
|
||||
|
||||
### 7. Make snapshots provider- and profile-exact
|
||||
|
||||
Analysis snapshot v2 replaces Hopper loader arguments with an explicit binding
|
||||
Analysis snapshots replace Hopper loader arguments with an explicit binding
|
||||
commitment:
|
||||
|
||||
```text
|
||||
snapshot_version: 2
|
||||
target: digest + generic format/kind + selected architecture
|
||||
binding: provider identity + analysis profile commitment
|
||||
entries: exact immutable queries
|
||||
@@ -321,7 +318,7 @@ A cached entry is reusable only when all of the following match exactly:
|
||||
|
||||
- target digest, generic format and kind, and selected architecture;
|
||||
- provider ID and provider version;
|
||||
- profile schema versions and profile digest;
|
||||
- profile digest;
|
||||
- operation and canonical parameters; and
|
||||
- the existing cacheability and immutability rules.
|
||||
|
||||
@@ -330,20 +327,20 @@ entries from the previous binding. Auxiliary operations are cacheable only if
|
||||
their provider defines an equivalent explicit profile commitment; otherwise
|
||||
they remain Evidence but not snapshot entries.
|
||||
|
||||
Snapshot v1 is not silently upgraded. Its `loader_args` cannot prove which
|
||||
engine version, defaults, or analyzer settings produced the entries. Importing
|
||||
v1 returns a typed incompatibility with a recapture instruction. Its embedded
|
||||
Evidence bundle may still be imported separately through the existing explicit
|
||||
Evidence import flow; it is never promoted into a v2 analysis cache.
|
||||
Snapshot v1 cannot be parsed as the current snapshot format. Its `loader_args`
|
||||
cannot prove which engine version, defaults, or analyzer settings produced the
|
||||
entries, so snapshots must be recaptured before they can be used as a cache.
|
||||
An embedded Evidence bundle can still be imported separately through the
|
||||
Evidence import flow; it is never promoted into an analysis cache.
|
||||
|
||||
### 8. Add profile commitments to new deep Evidence without rejecting legacy Evidence v2
|
||||
### 8. Add profile commitments to new deep Evidence while preserving legacy records
|
||||
|
||||
Evidence remains schema version 2. A backward-compatible optional
|
||||
`analysis_profile` field uses the canonical envelope above. Every newly created
|
||||
deep-analysis observation must include it, and its provider ID and version must
|
||||
match the record's provider. The field participates in the Evidence semantic ID.
|
||||
The optional `analysis_profile` field uses the canonical envelope above. Every
|
||||
newly created deep-analysis observation must include it, and its provider ID
|
||||
and version must match the record's provider. The field participates in the
|
||||
Evidence semantic ID.
|
||||
|
||||
Existing Evidence v2 records without `analysis_profile` remain valid and keep
|
||||
Existing Evidence records without `analysis_profile` remain valid and keep
|
||||
their existing IDs. Absence on a legacy deep record means the analysis profile
|
||||
is unknown; REA does not infer or enrich it during import. Such evidence can be
|
||||
displayed and cited but cannot establish profile compatibility, seed snapshot
|
||||
@@ -459,9 +456,9 @@ snapshot's provider.
|
||||
| `open_binary` and CLI structured `loaderArgs` output | Retained as a deprecated Hopper-only compatibility projection for 1.x; the analysis profile is authoritative |
|
||||
| `BinaryTarget.loaderArgs` | Removed; Hopper derives them inside its adapter and commits normalized semantics in the profile |
|
||||
| Legacy `.hop` input | Classified generically as an analysis database and accepted only by Hopper |
|
||||
| Snapshot v1 | Rejected for cache replay with explicit recapture guidance; embedded Evidence may be imported separately |
|
||||
| Existing Evidence v2 without a profile | Accepted unchanged; profile compatibility is unknown |
|
||||
| New deep-analysis Evidence v2 | Must include `analysis_profile`; its semantic ID commits the field |
|
||||
| Snapshot v1 | Not accepted as the current snapshot format; recapture before cache replay |
|
||||
| Existing Evidence without a profile | Accepted unchanged; profile compatibility is unknown |
|
||||
| New deep-analysis Evidence | Must include `analysis_profile`; its semantic ID commits the field |
|
||||
| Existing status fields | Retained for 1.x with additive binding and candidate fields |
|
||||
| Provider failure after selection | Returned from the selected provider; no transparent retry through another engine |
|
||||
|
||||
@@ -485,7 +482,6 @@ with a separately documented major contract change.
|
||||
| Same target is reopened without a selector | Existing binding remains unchanged |
|
||||
| Same bytes are opened with a different profile | Real provider switch and a separate snapshot partition |
|
||||
| Snapshot provider or profile differs from the binding | Typed `EvidenceIntegrityError` with `profile_mismatch`; nothing is imported |
|
||||
| Snapshot v1 is supplied | Typed incompatibility with recapture or explicit Evidence-import guidance |
|
||||
| Artifact-only target has no deep candidate | Target opens unbound; disjoint artifact capabilities remain usable |
|
||||
| Environment prefers Ghidra for an artifact-only target | Deep selection is `not_applicable`; auxiliary operations remain usable |
|
||||
| Concrete deep provider is requested for artifact-only data | Typed `target_unsupported`; the request is not silently ignored |
|
||||
@@ -504,7 +500,7 @@ Positive consequences:
|
||||
Costs and tradeoffs:
|
||||
|
||||
- Multi-provider hosts must choose when more than one candidate is usable.
|
||||
- Snapshot v1 cannot safely serve as a cache after this migration.
|
||||
- Snapshots must be recaptured when their provider or profile binding changes.
|
||||
- Providers must expose deterministic availability, support, version, and
|
||||
profile resolution before they can be admitted.
|
||||
- Status and error contracts gain additive structured metadata.
|
||||
@@ -561,14 +557,15 @@ The first six implementation stages are shipped:
|
||||
4. Ghidra availability, target/profile resolution, doctor checks, and a private
|
||||
read-only headless session were admitted without declaring operations.
|
||||
5. Program identity, procedures, strings, symbols, memory blocks,
|
||||
address/name resolution, containing-procedure resolution, and bounded search
|
||||
address/name resolution, containing-procedure resolution, and search
|
||||
were admitted with exact wire schemas and real debug/stripped ELF
|
||||
conformance.
|
||||
6. Function metadata, persistent bounded decompilation, assembly, resolved
|
||||
6. Function metadata, persistent decompilation, assembly, resolved
|
||||
callers/callees, typed references, xrefs, CFG, and complete function dossiers
|
||||
were admitted. A bounded per-Program queue serializes API access, and real
|
||||
conformance covers x86-64 and AArch64 ELF, PE, Mach-O, stripped symbols,
|
||||
targetless indirect calls, cancellation, deadlines, and cleanup.
|
||||
were admitted. A serial per-Program queue keeps API access ordered without a
|
||||
fixed queue length or per-operation deadline. Real conformance covers
|
||||
x86-64 and AArch64 ELF, PE, Mach-O, stripped symbols, targetless indirect
|
||||
calls, cancellation, and cleanup.
|
||||
|
||||
Future stages may deepen format and indirect-flow coverage, but must continue
|
||||
to compare normalized semantics rather than provider-specific pseudocode or
|
||||
|
||||
@@ -168,9 +168,10 @@ requires an active permission grant and literal per-call approval bound to the
|
||||
exact replay plan. Project, session, and administrator grants may satisfy the
|
||||
permission-policy layer, but none replace that per-run approval.
|
||||
|
||||
Reproducer export is a separate host write after the sandbox has stopped. It
|
||||
uses the existing bounded `evidence_write` authority and a separate export
|
||||
approval; `javascript_replay` alone never writes a host file.
|
||||
Reproducer export is a separate host write after the sandbox has stopped. This
|
||||
ADR's root-scoped `evidence_write` authority was removed; current exports use
|
||||
the path supplied by the caller, while replay still waits for complete sandbox
|
||||
cleanup before writing.
|
||||
|
||||
### 2. Admit extracted modules only
|
||||
|
||||
@@ -226,7 +227,7 @@ The plan must disclose at least:
|
||||
projection; and
|
||||
- expected Evidence and cleanup behavior.
|
||||
|
||||
Execution requires both `approved: true` and the exact `plan_digest`. REA
|
||||
Execution requires the exact `plan_digest`. REA
|
||||
rebuilds the plan immediately before admission. Any changed file, runtime,
|
||||
sandbox executable, policy, stub, input, limit, or environment commitment
|
||||
returns `plan_stale` before application code starts. Approval is not transferable
|
||||
@@ -321,7 +322,8 @@ Every limit has a conservative product default. The v1 plan and result commit:
|
||||
- cgroup memory, swap, task, and CPU ceilings;
|
||||
- Node/V8 heap ceiling as defense in depth;
|
||||
- private tmpfs bytes;
|
||||
- module count and aggregate bytes, case count, and input bytes;
|
||||
- serialized worker-protocol bytes, aggregate module bytes, and case-input
|
||||
bytes; preset-generated cases also have a fixed preflight count ceiling;
|
||||
- stdout, stderr, exception, stack, and aggregate retained-output bytes; and
|
||||
- structured-result depth and node count.
|
||||
|
||||
@@ -332,7 +334,7 @@ partial successful return.
|
||||
|
||||
### 7. Preserve exact provenance and the right Evidence authority
|
||||
|
||||
A run that reaches worker admission produces Evidence v2 with:
|
||||
A run that reaches worker admission produces Evidence with:
|
||||
|
||||
- provider `rea-javascript-replay` and a concrete implementation version;
|
||||
- operation `run_controlled_replay`;
|
||||
@@ -406,7 +408,7 @@ Failure at one cleanup step does not skip the remaining steps.
|
||||
The canonical replay manifest commits:
|
||||
|
||||
```text
|
||||
schema and policy versions
|
||||
plan structure and policy identity
|
||||
REA/provider/runtime/sandbox executable identities and digests
|
||||
module aliases, dependency graph, bytes digests, and source references
|
||||
stub IDs, versions, source digests, and caller-provided stub digests
|
||||
|
||||
@@ -189,7 +189,7 @@ or a CLR load. Complete-coverage claims require every applicable bounded region
|
||||
to have been admitted; otherwise coverage is partial, unknown, or unavailable
|
||||
with exact offsets and reasons.
|
||||
|
||||
The input path remains under REA's approved investigation roots. Local artifact
|
||||
The caller selects the local input path directly. Local artifact
|
||||
paths, digests, MVIDs, tokens, malformed offsets, and analysis metadata are
|
||||
actionable diagnostics and are not secrets. Credentials, authorization data,
|
||||
and genuine secrets remain redacted.
|
||||
@@ -203,7 +203,7 @@ artifact: canonical path + byte length + SHA-256
|
||||
component: container identity + component path/name + component SHA-256, when applicable
|
||||
classification: container + runtime family + implementation form + architecture
|
||||
assembly/module: simple name + version + culture + public-key identity + module name + MVID
|
||||
producer: provider ID + exact version + parser/profile schema + profile digest
|
||||
producer: provider ID + exact version + analysis-affecting parameters + profile digest
|
||||
```
|
||||
|
||||
An assembly-qualified name does not replace the artifact digest or MVID.
|
||||
@@ -218,7 +218,7 @@ declaring type + normalized CLI signature
|
||||
build-local metadata token
|
||||
RVA/file extent when present
|
||||
raw CIL byte length + SHA-256 when the body is admitted
|
||||
member-result schema version + decoded-instruction-tuple SHA-256 when CIL is valid
|
||||
decoded-instruction-tuple SHA-256 when CIL is valid
|
||||
separate max-stack, locals-token, and exception-region observations
|
||||
```
|
||||
|
||||
@@ -232,9 +232,9 @@ The caller-visible field remains named `normalized_il_sha256`, but its shipped
|
||||
v1 meaning is deliberately narrower than a canonical semantic CIL identity.
|
||||
For a completely decoded method, REA serializes the instruction-order array of
|
||||
`[opcode, operand_kind, operand]` tuples with JavaScript `JSON.stringify`,
|
||||
encodes that text as UTF-8, and reports its lowercase SHA-256. The member
|
||||
result's `schema_version: 1` scopes this convention; there is no independent
|
||||
normalized-CIL algorithm-version field in v1.
|
||||
encodes that text as UTF-8, and reports its lowercase SHA-256. The tuple
|
||||
projection is defined here; the member result carries only the described
|
||||
normalized digest.
|
||||
|
||||
Operands retain the current decoder projection: metadata and user-string
|
||||
operands are build-local lowercase hexadecimal tokens, scalar branch operands
|
||||
@@ -258,8 +258,8 @@ normalized-CIL schema.
|
||||
|
||||
Metadata tokens are reported because they are precise coordinates inside one
|
||||
module. Cross-version identity never consists of a token alone. A structural
|
||||
match cites both build commitments, both local tokens, the v1 tuple inputs, the
|
||||
member-result schema version, and all competing candidates. V1 cannot cite a
|
||||
match cites both build commitments, both local tokens, the v1 tuple inputs, and
|
||||
all competing candidates. V1 cannot cite a
|
||||
separate matching-algorithm version because none is exposed.
|
||||
|
||||
### 5. Separate five evidence layers
|
||||
@@ -386,7 +386,7 @@ requires its own threat model and admission decision.
|
||||
- Managed addresses distinguish file offsets, RVAs, native virtual addresses,
|
||||
metadata tokens, and CIL offsets. They are not interchangeable.
|
||||
- Cache compatibility is exact on artifact/component commitment, member-result
|
||||
schema, parser profile, and producer version. For shipped v1,
|
||||
fields, parser profile, and producer version. For shipped v1,
|
||||
`normalized_il_sha256` has no independent algorithm-version field.
|
||||
- Provider-neutral workflows may compose canonical managed findings with the
|
||||
one native binding, but every source operation retains its actual producer.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Apple Application Inventory Projection
|
||||
|
||||
REA can project one or more authenticated `inventory_artifact` Evidence records
|
||||
for the same IPA into an Apple application inventory. The projection is
|
||||
execution-free: it reports exact paths, content hashes, detected formats,
|
||||
runtime-family hints, and path-based bridge hypotheses. It does not parse plist
|
||||
or CMS semantics or claim observed runtime calls.
|
||||
|
||||
The result includes every component in each inventory category and every
|
||||
JavaScript-to-native bridge candidate pair. It has no caller-selected component
|
||||
budget, prefix truncation, or omitted-count fields. Duplicate inventory pages
|
||||
are merged by artifact identity and occurrence path; their Evidence IDs remain
|
||||
attached as source Evidence.
|
||||
|
||||
Coverage is `complete-within-inventory` when the supplied pages reconstruct the
|
||||
complete authenticated inventory, and `partial` otherwise. A partial result
|
||||
states that absence is unknown. Component and bridge-candidate arrays are still
|
||||
the complete projection of the inventory that was supplied.
|
||||
@@ -16,8 +16,7 @@ graph TD
|
||||
APP["src/application/<br/>Shared CLI/MCP workflows<br/>enhanced analysis and investigations"]
|
||||
SESSION["SessionProviderRouter + BinarySession<br/>one immutable deep binding per target<br/>disjoint auxiliary routing"]
|
||||
REGISTRY["AnalysisProviderRegistry<br/>sorted deep candidates<br/>deterministic selection, no fallback"]
|
||||
WORKSPACE["Investigation workspace store<br/>approved roots, canonical JSON<br/>atomic CAS revisions and locks"]
|
||||
JS_RECON["JavaScript artifact reconstruction<br/>bounded directory/ASAR reads<br/>AST-only bundle projection"]
|
||||
JS_RECON["JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
|
||||
end
|
||||
|
||||
subgraph "Provider Layers"
|
||||
@@ -29,7 +28,7 @@ graph TD
|
||||
end
|
||||
|
||||
subgraph "Provider Process Foundation"
|
||||
PROCESS["src/process/<br/>owned process groups, private roots<br/>deadlines, bounded diagnostics, cleanup"]
|
||||
PROCESS["src/process/<br/>owned process groups, private roots<br/>deadlines, bounded file reads and diagnostics, cleanup"]
|
||||
end
|
||||
|
||||
subgraph "Contracts Layer"
|
||||
@@ -37,7 +36,7 @@ graph TD
|
||||
end
|
||||
|
||||
subgraph "Domain Layer"
|
||||
DOMAIN["src/domain/<br/>Pure side-effect-free modules<br/>evidence, comparisons, application graphs, workspace commitments"]
|
||||
DOMAIN["src/domain/<br/>Pure side-effect-free modules<br/>evidence, comparisons, application graphs"]
|
||||
end
|
||||
|
||||
subgraph "Configuration"
|
||||
@@ -59,7 +58,6 @@ graph TD
|
||||
REGISTRY --> HOPPER
|
||||
REGISTRY --> GHIDRA
|
||||
SESSION --> AUX
|
||||
APP --> WORKSPACE
|
||||
APP --> JS_RECON
|
||||
APP --> INSTALL
|
||||
SERVER --> CONTRACTS
|
||||
|
||||
+46
-46
@@ -1,19 +1,21 @@
|
||||
# Website observation with CDP
|
||||
|
||||
REA can attach to a user-owned Chrome-family browser through the Chrome DevTools Protocol (CDP) and produce bounded Evidence v2 about an existing page. This is a passive reverse-engineering capability, not a general browser automation or remote-control surface.
|
||||
REA can attach to a user-owned Chrome-family browser through the Chrome DevTools Protocol (CDP) and produce bounded Evidence about an existing page. This is a passive reverse-engineering capability, not a general browser automation or remote-control surface.
|
||||
|
||||
## Shipped surfaces
|
||||
|
||||
- `list_browser_targets` / `rea list-browser-targets` discovers page targets whose current URL matches an approved exact origin.
|
||||
- `inspect_web_page` / `rea inspect-web-page` captures bounded DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally storage key names, redacted storage-content fingerprints, or script sources.
|
||||
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally storage key names, redacted storage-content fingerprints, or script sources.
|
||||
- `analyze_web_bundle` / `rea analyze-web-bundle` parses approved script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally approved source-map/original-source evidence.
|
||||
- `observe_web_session` / `rea observe-web-session` arms a bounded window for an external user action and records ordered reload, SPA navigation, redirect, failure, lifecycle, and target-termination metadata.
|
||||
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to inventory page registrations. REA never exposes `WebMCP.invokeTool`.
|
||||
- `observe_web_session` / `rea observe-web-session` arms an observation window of the requested duration for an external user action and records ordered reload, SPA navigation, redirect, failure, lifecycle, and target-termination metadata.
|
||||
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete structural input-schema summary inline. REA never exposes `WebMCP.invokeTool`; malformed protocol payloads fail explicitly instead of returning incomplete tool metadata.
|
||||
- `compare_web_captures` / `rea compare-web-captures` compares passive captures or exact-step-aligned browser scenarios. Scenario results commit reproducible literal normalization and expose alignment failures plus artifact-level action, screenshot, DOM, accessibility, URL, history, storage, and event differences. Missing or truncated evidence is never treated as equivalence.
|
||||
- `capture_web_screenshot` / `rea capture-web-screenshot` returns an explicitly approved, bounded, content-addressed visible-viewport PNG.
|
||||
- `capture_web_screenshot` / `rea capture-web-screenshot` returns an explicitly approved, content-addressed visible-viewport PNG.
|
||||
- `compare_web_screenshots` / `rea compare-web-screenshots` performs bounded local PNG pixel comparison without OCR or external services.
|
||||
- Every surface has equivalent CLI and MCP contracts and returns Evidence v2 provenance.
|
||||
- MCP session results are retained as `rea://evidence/{evidenceId}` resources. One-shot CLI output is not retained by a long-lived REA session.
|
||||
- Every surface has equivalent CLI and MCP contracts and returns Evidence provenance.
|
||||
- MCP tools return complete results inline and include the Evidence ID. Session evidence can be exported with `export_evidence_bundle`.
|
||||
|
||||
Target discovery returns the complete approved target array in one result.
|
||||
|
||||
Electron `file://` pages use a separate provider, permission capability, and root model; see [electron-observation.md](electron-observation.md).
|
||||
Existing static application Evidence and passive web/Electron captures can be
|
||||
@@ -47,7 +49,7 @@ export REA_BROWSER_ALLOWED_ORIGINS_JSON='["http://127.0.0.1:3000"]'
|
||||
|
||||
`REA_BROWSER_ALLOWED_ORIGINS_JSON` accepts at most 32 exact HTTP(S) origins. Paths, credentials, queries, fragments, and wildcards are rejected. An input must stay within both this administrator ceiling and any active project/session/once grant.
|
||||
|
||||
Configuration is reloadable through the existing `SIGHUP` permission-policy path. A request still requires `approved: true`; configuration alone does not make a tool call implicit.
|
||||
Configuration is reloadable through the existing `SIGHUP` permission-policy path. Each request is checked against the configured endpoint, origin, and permission grants.
|
||||
|
||||
Inspect the exact effective scope without contacting the browser:
|
||||
|
||||
@@ -64,9 +66,8 @@ rea policy explain browser_observe \
|
||||
Configured origins are used by default. `--allowed-origins` can request a narrower set that still fits the configured ceiling.
|
||||
|
||||
```bash
|
||||
rea list-browser-targets http://127.0.0.1:9222 --approved --json
|
||||
rea list-browser-targets http://127.0.0.1:9222 --json
|
||||
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID \
|
||||
--approved \
|
||||
--observation-ms 1000 \
|
||||
--json
|
||||
```
|
||||
@@ -75,34 +76,33 @@ Static bundle analysis and a user-driven observation window are separate operati
|
||||
|
||||
```bash
|
||||
rea analyze-web-bundle http://127.0.0.1:9222 TARGET_ID \
|
||||
--approved --source-capture-approved --json
|
||||
--json
|
||||
rea observe-web-session http://127.0.0.1:9222 TARGET_ID \
|
||||
--approved --observation-ms 10000 --json
|
||||
--observation-ms 10000 --json
|
||||
rea discover-webmcp-tools http://127.0.0.1:9222 TARGET_ID \
|
||||
--approved --json
|
||||
--json
|
||||
```
|
||||
|
||||
Accessibility names/descriptions, console primitive text, JSON body shapes, WebSocket shapes, script content, and storage key names require separate opt-ins. Console/body/WebSocket capture additionally requires its matching approval flag:
|
||||
Accessibility names/descriptions, console primitive text, JSON body shapes, WebSocket shapes, script content, and storage key names are selected independently:
|
||||
|
||||
```bash
|
||||
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID \
|
||||
--approved \
|
||||
--include-accessibility-text \
|
||||
--include-console-text --console-text-approved \
|
||||
--include-json-body-shapes --json-body-schema-approved \
|
||||
--include-websocket-shapes --websocket-shape-approved \
|
||||
--include-console-text \
|
||||
--include-json-body-shapes \
|
||||
--include-websocket-shapes \
|
||||
--include-script-sources \
|
||||
--include-storage-keys --include-storage-fingerprints \
|
||||
--json
|
||||
```
|
||||
|
||||
Accessibility text and script content may contain application secrets and become part of the returned Evidence. Accessibility and console text have independent per-field and aggregate UTF-8 byte limits; credential-shaped console substrings are redacted. JSON and WebSocket captures retain paths, types, counts, and truncation only, never values or examples. Storage values remain redacted even when key-name capture is approved. `--include-storage-fingerprints` separately approves SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
|
||||
Accessibility text and script content may contain application secrets and become part of the returned Evidence. Credential-shaped console substrings are redacted. JSON and WebSocket captures retain complete paths, types, and counts, never values or examples. Storage values remain redacted even when key-name capture is requested. `--include-storage-fingerprints` requests SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
|
||||
|
||||
Screenshot pixels require both `--approved` and `--screenshot-approved`:
|
||||
Screenshot capture returns the complete visible viewport as an inline image artifact:
|
||||
|
||||
```bash
|
||||
rea capture-web-screenshot http://127.0.0.1:9222 TARGET_ID \
|
||||
--approved --screenshot-approved --json
|
||||
--json
|
||||
```
|
||||
|
||||
## MCP input
|
||||
@@ -111,22 +111,16 @@ rea capture-web-screenshot http://127.0.0.1:9222 TARGET_ID \
|
||||
{
|
||||
"cdp_endpoint": "http://127.0.0.1:9222",
|
||||
"allowed_origins": ["http://127.0.0.1:3000"],
|
||||
"target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS",
|
||||
"approved": true,
|
||||
"observation_ms": 500,
|
||||
"include_accessibility_text": false,
|
||||
"include_console_text": false,
|
||||
"console_text_approved": false,
|
||||
"include_json_body_shapes": false,
|
||||
"json_body_schema_approved": false,
|
||||
"include_websocket_shapes": false,
|
||||
"websocket_shape_approved": false,
|
||||
"include_script_sources": false,
|
||||
"include_storage_keys": false,
|
||||
"include_storage_fingerprints": false
|
||||
"target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"
|
||||
}
|
||||
```
|
||||
|
||||
The omitted observation window and capture options use conservative defaults.
|
||||
Set an option only when the investigation needs that additional content.
|
||||
Collection counts are not capped. Per-value validation, the selected observation
|
||||
window, and CDP protocol limits still apply; any incomplete or truncated values
|
||||
are reported in the result.
|
||||
|
||||
Call `list_browser_targets` first because target IDs are browser-instance-specific. REA rechecks the selected target's current type and origin immediately before attaching.
|
||||
|
||||
REA establishes an authorized main-frame boundary before enabling Runtime, Debugger, or Network observation, rechecks it before document capture, and validates it again afterward. If the main frame navigates during final capture, REA discards the mixed result and returns `target_changed`.
|
||||
@@ -137,28 +131,34 @@ REA removes sensitive values before normalized event data is retained:
|
||||
|
||||
- URLs retain a bounded path and at most 256 bounded query parameter names, replace every retained query value with `[REDACTED]`, and remove credentials and fragments.
|
||||
- DOM snapshots retain node types, node names, value lengths, and attribute names, but not text or attribute values.
|
||||
- Accessibility structure and roles are retained by default, but names and descriptions require `include_accessibility_text: true` and remain byte-bounded.
|
||||
- Accessibility structure and roles are retained by default, while names and descriptions require `include_accessibility_text: true`.
|
||||
- Network observations retain method, status, MIME type, size, type, initiator stack location, and redacted URLs. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
|
||||
- Request/response bodies are not requested or parsed by default. With independent approval, only allowed-origin JSON media types are read within per-body and aggregate budgets, converted immediately to value-free property paths/types, and discarded. `Network.getResponseBody` is never sent without that approval.
|
||||
- Console observations with an approved stack source retain call type, argument types, timestamp, and redacted source location. With independent approval, only already-delivered primitive values are retained after credential redaction and byte bounds; objects, getters, and remote properties are never expanded.
|
||||
- WebSocket observations retain direction, opcode, and payload byte length. With independent approval, bounded text frames are classified as text or value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
|
||||
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` approval. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
|
||||
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Source content is omitted unless explicitly requested and becomes a self-verifying `rea://web-content/sha256/...` artifact subject to per-script and aggregate byte limits.
|
||||
- Request/response bodies are not requested or parsed by default. When selected, only allowed-origin JSON media types are read, converted immediately to complete value-free property paths/types, and discarded. `Network.getResponseBody` is never sent unless JSON body shape capture is selected.
|
||||
- Console observations with a stack source retain call type, argument types, timestamp, and redacted source location. When console text capture is selected, already-delivered primitive values are retained after credential redaction; objects, getters, and remote properties are never expanded.
|
||||
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
|
||||
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
|
||||
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Source content is omitted unless explicitly requested and is returned inline as a self-verifying artifact with its SHA-256 digest.
|
||||
|
||||
Cross-origin frames, resources, scripts, events, and workers are excluded unless their exact origins are also approved. Excluded target details are counted without being exposed.
|
||||
Retained workers include bounded opener-target and parent-frame IDs when CDP
|
||||
Retained workers include validated opener-target and parent-frame IDs when CDP
|
||||
provides them; these relationships do not expand the exact-origin boundary or
|
||||
prove static module ownership.
|
||||
|
||||
## Completeness and limits
|
||||
|
||||
The default observation window is 500 ms and the maximum is 10 seconds. Frames (200), DOM nodes (2,000), accessibility nodes (2,000), scripts (200), resources (2,000), workers (500), and each storage key/name inventory (1,000) have conservative defaults and caller-visible hard bounds. Script source bytes, network events, console events, and WebSocket events are bounded separately. The CLI exposes the same limits as kebab-case options such as `--max-frames` and `--max-storage-keys`.
|
||||
The default inspection window is 500 ms; callers choose its duration. The
|
||||
capture returns all in-scope frames, DOM and accessibility nodes, scripts,
|
||||
resources, workers, storage names, and observed network, console, and WebSocket
|
||||
events. Full selected text, script sources, and JSON shapes are returned inline.
|
||||
The result distinguishes
|
||||
`complete_within_window`, `policy_filtered`, `attach_limited`, and `truncated`
|
||||
coverage. It reports excluded items and any values omitted or truncated by
|
||||
validation; it does not drop records to meet an internal collection quota.
|
||||
Disallowed-origin entries are filtered before retention.
|
||||
|
||||
Every result distinguishes `complete_within_window`, `policy_filtered`, `attach_limited`, and `truncated` coverage. It names affected sections and reports sparse excluded-item and per-event-type dropped counts rather than implying exhaustive capture. Disallowed-origin entries are filtered before collection-limit accounting, so they cannot consume an approved origin's output budget.
|
||||
CDP discovery, WebSocket connection, and each command have a 5-second timeout. Version discovery is capped at 64 KiB, target discovery at 2 MiB, each WebSocket message at 16 MiB, and correlated pending commands at 128. Target and navigation-event inventories have no separate count cap. These transport bounds protect protocol framing and request correlation; they do not silently trim returned findings. Malformed unsolicited protocol events poison the connection so later commands fail closed.
|
||||
|
||||
CDP discovery, WebSocket connection, and each command have a 5-second timeout. Version discovery is capped at 64 KiB, target discovery at 2 MiB and 1,000 targets, each WebSocket message at 16 MiB, and correlated pending commands at 128. Malformed unsolicited protocol events poison the connection so later commands fail closed.
|
||||
|
||||
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Source maps are not fetched by inspection. Bundle analysis can fetch them only after separate approval, with exact-origin redirect checks, no credentials/cookies/referrer, per-map and aggregate byte limits, a total map-count limit, v3 validation, and bounded generated/original mappings. Source-map results report requested, processed, and dropped counts rather than hiding budget truncation.
|
||||
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Source maps are not fetched by inspection. Bundle analysis can fetch them only after separate approval, with exact-origin redirect checks and no credentials, cookies, or referrer. Every requested map, mapping, original source, AST node, and finding from parsed content is returned inline; failed maps remain explicitly unavailable and make bundle completeness partial.
|
||||
|
||||
## Non-goals and threat model
|
||||
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
# Browser scenario contract
|
||||
|
||||
`capture_browser_scenario` accepts the versioned provider-neutral
|
||||
`capture_browser_scenario` accepts the provider-neutral
|
||||
`browserScenarioSchema` through both MCP and the
|
||||
`capture-browser-scenario INPUT_JSON` CLI command. Version 1 is declarative and
|
||||
bounded: it admits a fixed action vocabulary, exact HTTP(S) origins,
|
||||
deterministic browser settings, explicit storage and request replay, and finite
|
||||
capture limits. `INPUT_JSON` may be inline JSON or a JSON file path.
|
||||
`capture-browser-scenario INPUT_JSON` CLI command. The request is declarative:
|
||||
it admits a fixed action vocabulary, exact HTTP(S) origins, deterministic
|
||||
browser settings, explicit storage and request replay, and provider-owned
|
||||
liveness deadlines. `INPUT_JSON` may be inline JSON or a JSON file path.
|
||||
|
||||
The minimal request contains only browser launch/connect authority, `start_url`,
|
||||
`allowed_origins`, and at least one `actions` entry. Environment settings,
|
||||
empty storage, disabled request replay, the required redaction policy, and a
|
||||
final sanitized URL capture are supplied by default. Add secret declarations,
|
||||
storage seeds, replay routes, query redaction names, and additional artifact or
|
||||
event capture only when the investigation needs them. Action, secret, storage,
|
||||
and replay-route counts are not capped. Duration, action, and navigation
|
||||
timeouts remain fixed provider-owned liveness limits; the request does not
|
||||
accept a caller-controlled `limits` object.
|
||||
|
||||
The browser boundary is part of the contract. Launch mode requires a
|
||||
caller-selected absolute executable, a provider-owned temporary profile,
|
||||
@@ -30,14 +40,18 @@ route IDs, undeclared or unused secrets, and provider-owned replay headers fail
|
||||
validation. HTTP and WebSocket routing enforces the same declared scope. Exact
|
||||
replay may abort unmatched requests or pass through only approved origins.
|
||||
|
||||
The result is Evidence v2 with an initial state followed by one record per
|
||||
The result is Evidence with an initial state followed by one record per
|
||||
declared action. Each step reports action status, elapsed time, sanitized URLs,
|
||||
event bounds, and independently typed capture states for screenshot, DOM,
|
||||
accessibility, URL, history, and storage. Console, page-error, network,
|
||||
WebSocket, frame, worker, popup, and cancelled-download events are attributed to
|
||||
steps and bounded by the scenario limits. Missing and truncated sections remain
|
||||
explicit and make the capture ineligible for equality claims. Attach-mode
|
||||
captures also declare the unavoidable pre-attach event gap.
|
||||
steps and all observed events and requested artifacts are returned inline.
|
||||
There are no event, frame, worker, popup, WebSocket, DOM-node,
|
||||
accessibility-node, storage-entry, screenshot-count, or cumulative
|
||||
metadata-byte caps. Requested artifacts and observed events are returned
|
||||
inline without application-defined size ceilings. Other missing sections
|
||||
remain explicit and make the capture ineligible for equality claims.
|
||||
Attach-mode captures also declare the unavoidable pre-attach event gap.
|
||||
|
||||
Browser automation is disabled by default. The administrator ceiling is
|
||||
configured with:
|
||||
@@ -75,6 +89,6 @@ capture fields and are preserved in the result.
|
||||
Each aligned step reports changed, unchanged, or unknown. Changed and unknown
|
||||
action, screenshot, DOM, accessibility, URL, history, storage, and event
|
||||
artifacts carry their normalized before/after digests and capture states.
|
||||
`max_changes` bounds retained artifact records while total and omitted counts
|
||||
remain visible. Missing, truncated, or mismatched capture coverage remains
|
||||
unknown instead of being treated as equality.
|
||||
Every changed or unknown artifact record is returned inline for each aligned
|
||||
step. Missing, truncated, or mismatched capture coverage remains unknown
|
||||
instead of being treated as equality.
|
||||
|
||||
@@ -79,8 +79,8 @@ Each left/right run is retained as its own `observed` source Evidence. A
|
||||
differential envelope is `derived` and links those source Evidence IDs, so the
|
||||
comparison never erases the underlying observations.
|
||||
|
||||
Optional `reproducer_export` is committed by the plan and requires both
|
||||
`approved: true` inside that object and separate `evidence_write` authority.
|
||||
Optional `reproducer_export` is committed by the plan and writes to its
|
||||
caller-supplied path after the sandbox has stopped.
|
||||
The owner-only manifest is written only after complete sandbox cleanup. Source
|
||||
bytes are excluded unless `include_sources: true` was explicitly approved.
|
||||
An export failure is retained in the result and does not erase a completed
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# Electron file-page observation
|
||||
|
||||
REA can attach to a user-owned Electron/Chromium CDP endpoint and inspect existing `file://` renderer pages without evaluating JavaScript or invoking Electron APIs. Electron observation is separate from website observation because filesystem roots, not HTTP origins, define its authority.
|
||||
REA can attach to a user-owned Electron/Chromium CDP endpoint and inspect existing `file://` renderer pages without evaluating JavaScript or invoking Electron APIs. The caller supplies the endpoint directly; REA accepts only literal-port loopback HTTP endpoints. Selecting an endpoint exposes every Electron target it serves, including local file paths and page metadata.
|
||||
|
||||
This passive runtime surface is distinct from the target-free static
|
||||
[`analyze_javascript_application`](javascript-artifact-reconstruction.md)
|
||||
workflow. Static analysis reads an approved ASAR or extracted directory under
|
||||
`REA_INVESTIGATION_INPUT_ROOTS_JSON`; it does not attach to CDP. Runtime
|
||||
observations and static inferences are never silently treated as the same fact.
|
||||
Use the separate
|
||||
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md) workflow
|
||||
@@ -17,43 +16,96 @@ The capability is disabled by default:
|
||||
|
||||
```bash
|
||||
export REA_ELECTRON_OBSERVE_ENABLED=true
|
||||
export REA_ELECTRON_CDP_ENDPOINTS_JSON='["http://127.0.0.1:9223"]'
|
||||
export REA_ELECTRON_FILE_ROOTS_JSON='["/Applications/Example.app/Contents/Resources"]'
|
||||
```
|
||||
|
||||
Endpoints accept only explicit-port loopback HTTP URLs. Roots are canonicalized by the shared permission authority. Each target, frame, script, and resource is independently converted from a hostless `file://` URL to a real path and checked after symlink resolution. UNC hosts, percent-encoded path separators, nonexistent paths, and root escapes are rejected.
|
||||
The capability is disabled by default. Its permission ceiling is loopback-only. REA accepts local hostless `file://` URLs that resolve to regular files; remote hosts, encoded path separators, and nonexistent paths are rejected. There is no separate filesystem-root configuration.
|
||||
|
||||
## Workflow
|
||||
|
||||
`list_electron_targets` returns every eligible target served by the supplied endpoint in one inline array.
|
||||
|
||||
For the MCP follow-up call, pass the same literal-loopback endpoint and the selected target ID to `inspect_electron_page`. REA rediscovers targets and validates the ID against the live endpoint before inspection.
|
||||
|
||||
```bash
|
||||
rea list-electron-targets http://127.0.0.1:9223 --approved --json
|
||||
rea list-electron-targets http://127.0.0.1:9223 --json
|
||||
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
|
||||
--approved --observation-ms 100 --json
|
||||
--observation-ms 100 --json
|
||||
```
|
||||
|
||||
Script content is excluded by default. Capturing it requires both flags and remains subject to per-script and aggregate byte budgets:
|
||||
Script content is excluded by default. Requesting it includes every authorized script source:
|
||||
|
||||
```bash
|
||||
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
|
||||
--approved \
|
||||
--include-script-sources \
|
||||
--source-capture-approved \
|
||||
--json
|
||||
```
|
||||
|
||||
The normalized result contains canonical local paths, bounded frame and DOM
|
||||
The normalized result contains canonical local paths, frame and DOM
|
||||
structure, resource metadata, stable script/resource identities, explicit
|
||||
completeness, and content-addressed approved source artifacts. Script metadata
|
||||
retains its execution-context frame ID when CDP supplies one. The capture also
|
||||
inventories authorized worker, service-worker, and shared-worker targets with
|
||||
bounded opener-target and parent-frame IDs. Worker discovery uses passive
|
||||
validated opener-target and parent-frame IDs. Worker discovery uses passive
|
||||
target metadata; REA does not attach to or execute code in those targets.
|
||||
|
||||
The default worker limit is 500 and the caller-visible maximum is 5,000. Like
|
||||
every target, frame, script, and resource, a worker URL must resolve beneath an
|
||||
approved canonical root before it is retained. Relationship IDs improve
|
||||
Collection counts and aggregate script-source bytes are not capped. Like every
|
||||
target, frame, script, and resource, a worker URL must resolve to a local
|
||||
file before it is retained. Relationship IDs improve
|
||||
attribution but do not prove which static module started a worker or that its
|
||||
work completed.
|
||||
|
||||
Inspection does not retain DOM values, execute renderer code, navigate, click,
|
||||
invoke Electron IPC, close a target, or terminate the application.
|
||||
|
||||
## Active Electron scenarios
|
||||
|
||||
Active Electron authority is separate and disabled by default. When enabled,
|
||||
REA launches an operator-approved Electron executable through the official
|
||||
Playwright Electron API, owns its lifetime, accepts bounded click/wait actions,
|
||||
window-targeted renderer reload/crash actions, and synthetic `open-url` or
|
||||
`second-instance` deep-link delivery. It records capture-scoped window and
|
||||
WebContents identities, process metrics, and IPC channel and value-shape metadata.
|
||||
Payload values are not retained.
|
||||
|
||||
Configure exact roots and run the real fixture verifier with an operator-owned
|
||||
Electron runtime:
|
||||
|
||||
```bash
|
||||
export REA_ELECTRON_AUTOMATE_ENABLED=true
|
||||
export REA_ELECTRON_AUTOMATE_AUTO_GRANT=false
|
||||
export REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON='["/absolute/path/to/runtime"]'
|
||||
export REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON='["/absolute/path/to/app"]'
|
||||
REA_ELECTRON_EXECUTABLE=/absolute/path/to/electron npm run verify:electron
|
||||
```
|
||||
|
||||
This capability actively launches and interacts with the target. It is not a
|
||||
passive CDP observation and must be granted separately as `electron_automate`.
|
||||
The default is fail-closed (`AUTO_GRANT=false`); an operator can instead issue
|
||||
a project/session/one-shot grant through the normal permission workflow. The
|
||||
owned process keeps normal host filesystem and network privileges, so this is
|
||||
an authority boundary and lifecycle boundary, not a sandbox.
|
||||
|
||||
The CLI and MCP surfaces accept the same schema. For a JSON request file:
|
||||
|
||||
```bash
|
||||
rea capture-electron-scenario scenario.json --json
|
||||
```
|
||||
|
||||
The result records action status and targets, correlated app/window/WebContents,
|
||||
preload, session, navigation, shell, permission, popup, download, protocol,
|
||||
native-addon, process, and IPC timeline events. IPC channels are capped at
|
||||
1,024 characters and argument-shape metadata at 32 entries; values are never
|
||||
retained. Renderer crash/restart and deep-link actions are synthetic
|
||||
scenario controls; their attempted and observed outcomes remain in the
|
||||
timeline. The active hook blocks and records external shell/navigation,
|
||||
permission, download, popup, updater, and OS-integration effects. The timeline
|
||||
is explicitly partial when attachment starts after application activity. The
|
||||
owned experiment has an internal 60-second deadline and a 5-second per-action
|
||||
timeout so hung work reaches process cleanup; these are lifecycle timeouts, not
|
||||
caller-selected capture limits.
|
||||
|
||||
Active Electron Evidence can also be supplied to
|
||||
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md). That
|
||||
projection is intentionally target-only and partial: it binds the approved
|
||||
application path and capture outcome to the static graph, while frames, scripts,
|
||||
workers, and execution claims remain unavailable until a separate passive runtime
|
||||
capture provides them.
|
||||
|
||||
@@ -661,45 +661,6 @@
|
||||
"required": ["code", "category", "message", "retryable", "remediation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "string", "const": "outside_approved_root" },
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
"cancelled",
|
||||
"timeout",
|
||||
"unavailable",
|
||||
"execution_failure"
|
||||
]
|
||||
},
|
||||
"message": { "type": "string", "minLength": 1 },
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
"additionalProperties": { "$ref": "#/$defs/__schema0" }
|
||||
}
|
||||
},
|
||||
"required": ["code", "category", "message", "retryable", "remediation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+105
-26
@@ -7,7 +7,7 @@ REA separates installing its CLI from configuring external software and agents.
|
||||
The recommended setup entrypoint is:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
If npm asks to download and run the package, that approval applies only to the
|
||||
@@ -15,12 +15,28 @@ current package-runner invocation. REA still prints its own setup plan and asks
|
||||
for separate approval before changing agent configuration or installing a
|
||||
product-owned component.
|
||||
|
||||
The explicit `@latest` request prevents a project dependency or stale npx cache
|
||||
entry from choosing the setup version.
|
||||
REA runs the version npm selected and never silently replaces an intentional
|
||||
version request after launch. To repair registrations written by an older
|
||||
release, rerun the command above and review the resulting setup plan.
|
||||
|
||||
For an intentional rollback, make the package request explicit:
|
||||
|
||||
```bash
|
||||
npm exec --yes --package=rea-agents@2.4.0 -- rea setup
|
||||
```
|
||||
|
||||
Setup continues to pin persistent MCP registrations to the exact version that
|
||||
performed setup. Running current setup later migrates unversioned or older
|
||||
managed registrations through the normal reviewed setup transaction.
|
||||
|
||||
REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
|
||||
|
||||
Running `npm install rea-agents` without `--global` installs the executable only
|
||||
in the current project's `node_modules/.bin`; it does not make `rea` available
|
||||
on the shell `PATH`. Use `npx rea-agents setup` for the guided setup journey,
|
||||
`npx -y rea-agents@latest` for unattended one-off commands, or install globally
|
||||
on the shell `PATH`. Use the explicit setup command above for the guided setup
|
||||
journey, `npx -y rea-agents@latest` for unattended one-off commands, or install globally
|
||||
with `npm install --global rea-agents` for a shell-visible `rea` command.
|
||||
|
||||
The optional curl wrapper installs only the global npm package:
|
||||
@@ -133,6 +149,16 @@ and digest, validates the application bundle, and atomically installs it to
|
||||
operator can choose its demo mode or activate an existing license. Homebrew and
|
||||
administrator access are not used.
|
||||
|
||||
The Hopper launcher action used by REA creates a document from an executable;
|
||||
its supported command-line interface does not attach to an already-open
|
||||
document. While another live REA session owns the same target and loader
|
||||
profile, a second session reports the owning run ID instead of opening a
|
||||
duplicate document. Closing the owning REA session releases this guard; Hopper
|
||||
keeps its document open. A later REA session can therefore open another
|
||||
document for the same target. REA does not currently identify, focus, or reuse
|
||||
that existing GUI document, and the supported launcher exposes no attach or
|
||||
reuse action for it.
|
||||
|
||||
On supported Linux distributions, approved setup verifies Hopper's official
|
||||
`.deb`, `.rpm`, or Arch package before invoking the native package manager.
|
||||
REA runs the supported demo build on a private Xvfb display and selects Hopper's
|
||||
@@ -142,24 +168,45 @@ desktop display. Unattended package-manager access requires
|
||||
mount, REA first verifies the conflict and then uses an unprivileged user and
|
||||
mount namespace with a private mode-1777 tmpfs over that directory only. The
|
||||
host mount and the rest of `/tmp` remain unchanged; this fallback never invokes
|
||||
`sudo`. `rea doctor --provider hopper --detail full --json` reports the selected
|
||||
`sudo`. `rea doctor --provider hopper --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 --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
|
||||
official Ghidra 12.1.2 release and a 64-bit full JDK 21. An experimental
|
||||
official Ghidra 12.1.4 release and a 64-bit full JDK 21. macOS is not an
|
||||
admitted Ghidra host. An experimental
|
||||
Windows x64 P0 supports approved native x86-64 PE applications. It supplies
|
||||
discovery, analysis-profile commitment, an isolated read-only headless session,
|
||||
ten bounded inventory/name/search operations, and nine function-analysis
|
||||
operations covering metadata, decompilation, assembly, resolved calls, typed
|
||||
references, xrefs, CFG, and dossiers. GUI state and analysis mutations remain
|
||||
unavailable through Ghidra.
|
||||
ten inventory/name/search operations and nine function-analysis operations,
|
||||
for 19 read-only operations total. Function analysis covers metadata,
|
||||
decompilation, assembly, resolved calls, typed references, xrefs, CFG, and
|
||||
dossiers. GUI state and analysis mutations remain unavailable through Ghidra.
|
||||
|
||||
Extract Ghidra and install the JDK outside REA, then export absolute paths:
|
||||
|
||||
```bash
|
||||
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.2_PUBLIC
|
||||
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
|
||||
export JAVA_HOME=/absolute/path/to/jdk-21 # optional if java/javac are on PATH
|
||||
rea doctor --json
|
||||
rea setup
|
||||
@@ -168,7 +215,7 @@ rea setup
|
||||
PowerShell configuration for Windows uses the same non-secret paths:
|
||||
|
||||
```powershell
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.2_PUBLIC"
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
|
||||
rea doctor --json
|
||||
rea providers --json
|
||||
@@ -188,30 +235,35 @@ environment entry in its plan, writes only after approval, and never downloads,
|
||||
installs, upgrades, or modifies Ghidra or Java.
|
||||
|
||||
Each verified session uses an ephemeral temporary project and isolated
|
||||
home/cache/config/temp paths. REA passes `-readOnly`, `-deleteProject`, a
|
||||
per-file analysis timeout, CPU and heap bounds, and its packaged Java bridge via
|
||||
`-scriptPath`; it never opens an existing user project. Linux uses a
|
||||
home/cache/config/temp paths. REA passes `-readOnly`, `-deleteProject`, uses
|
||||
Ghidra's default analysis and resource settings, and loads its packaged Java
|
||||
bridge via `-scriptPath`; it never opens an existing user project. Linux uses a
|
||||
current-user-only local bridge socket and descriptor. Windows P0 uses
|
||||
token-authenticated IPv4 loopback, a token-free endpoint record, and bounded
|
||||
process-tree termination. It does not yet prove private DACL,
|
||||
reparse-point-safe, or Job Object semantics; use only approved non-sensitive
|
||||
fixtures.
|
||||
|
||||
Operations begin only after default auto-analysis completes. A timeout fails
|
||||
the open rather than exposing partial analysis. One session contains exactly
|
||||
one imported Program; use `provider_id: "ghidra"`, `--provider ghidra`, or
|
||||
Operations begin only after default auto-analysis completes. The provider
|
||||
startup deadline fails the open rather than exposing partial analysis. One
|
||||
session contains exactly one imported Program; use `provider_id: "ghidra"`, `--provider ghidra`, or
|
||||
`REA_ANALYSIS_PROVIDER=ghidra` when both Hopper and Ghidra support the target.
|
||||
One persistent decompiler is owned by the Program, Ghidra API calls cross a
|
||||
bounded 32-request serial queue, and each native decompile is limited to 30
|
||||
seconds. Unresolved computed calls remain unknown, reference-kind provenance is
|
||||
preserved, and provider-specific pseudocode is never treated as original source
|
||||
or Hopper-equivalent text.
|
||||
One persistent decompiler is owned by the Program, and a serial queue keeps
|
||||
Ghidra API calls on the owning Program thread without a fixed queue length.
|
||||
Operations run until a result, caller cancellation, or provider shutdown; there
|
||||
is no fixed per-operation or response-size ceiling. Unresolved computed calls
|
||||
remain unknown, reference-kind provenance is preserved, and provider-specific
|
||||
pseudocode is never treated as original source or Hopper-equivalent text.
|
||||
|
||||
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra` from a source checkout to
|
||||
compile and analyze source-owned x86-64 debug/stripped ELF, AArch64 ELF,
|
||||
x86-64 PE, and x86-64 Mach-O conformance fixtures. The verifier expects `cc`,
|
||||
`clang`, and `lld-link` on `PATH`; `REA_CC`, `REA_CLANG`, and `REA_LLD_LINK` can
|
||||
select alternate command paths.
|
||||
compile and analyze debug and stripped x86-64 ELF fixtures on Linux x64. This lane needs only a host C compiler
|
||||
in addition to Ghidra and its JDK.
|
||||
|
||||
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra:cross-format` to add AArch64
|
||||
ELF, x86-64 PE, and x86-64 Mach-O fixture coverage. This separate lane needs
|
||||
`clang`, LLD, and `lld-link` on `PATH`; `REA_CLANG` and `REA_LLD_LINK` select
|
||||
alternate command paths. Missing cross-target tooling does not block the
|
||||
host-native Ghidra acceptance lane.
|
||||
|
||||
On a controlled Windows x64 runner, use
|
||||
`npm run verify:ghidra:windows`. The verifier generates a deterministic native
|
||||
@@ -221,3 +273,30 @@ target/snapshot/import digest identity, and requires complete runtime cleanup.
|
||||
## Diagnose, update, and remove
|
||||
|
||||
`rea doctor --json` is strictly read-only. `rea upgrade` updates only the npm installation that owns the running CLI. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
|
||||
|
||||
## MCP Registry
|
||||
|
||||
REA is published in the official MCP Registry as `io.github.morluto/rea`. Registry
|
||||
clients can discover the server and install the existing public `rea-agents` npm
|
||||
package; the Registry entry does not introduce a second distribution artifact.
|
||||
|
||||
For a client that supports Registry discovery, search for `io.github.morluto/rea`
|
||||
and select the npm package. The published metadata launches the existing stdio
|
||||
server through `npx` with the `mcp` command.
|
||||
|
||||
For a client that requires manual configuration, use:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use an exact `rea-agents@VERSION` in the arguments when a reproducible client
|
||||
configuration is required. `rea setup` writes the same package-runner shape and
|
||||
pins it to the exact version that performed setup.
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# Persistent investigation workspaces
|
||||
|
||||
REA can run a deterministic artifact investigation across two versions and
|
||||
persist its Evidence beyond a CLI process or MCP session. The CLI and MCP use
|
||||
the same application workflow.
|
||||
|
||||
## CLI
|
||||
|
||||
First allow the directory that will own the workspace, then explicitly approve
|
||||
the write:
|
||||
|
||||
```bash
|
||||
export REA_EVIDENCE_ROOTS_JSON='["/absolute/path/to/evidence"]'
|
||||
export REA_INVESTIGATION_INPUT_ROOTS_JSON='["/absolute/path/to/releases"]'
|
||||
|
||||
rea investigate-versions \
|
||||
/absolute/path/to/version-1 \
|
||||
/absolute/path/to/version-2 \
|
||||
/absolute/path/to/evidence/releases.json \
|
||||
--workspace-name releases \
|
||||
--yes
|
||||
```
|
||||
|
||||
The two versions may be regular artifacts or directories supported by the
|
||||
artifact graph provider. Both must resolve beneath an explicitly configured
|
||||
`REA_INVESTIGATION_INPUT_ROOTS_JSON` root. The workspace parent must already
|
||||
exist beneath `REA_EVIDENCE_ROOTS_JSON`. REA never creates an allowlisted root
|
||||
implicitly.
|
||||
|
||||
Use `--expected-revision N` when an external coordinator needs an explicit
|
||||
compare-and-swap guard. Traversal, byte, page, and comparison limits are also
|
||||
available as CLI options.
|
||||
|
||||
After a completed run returns its stable run ID, `--replay-run-id RUN_ID`
|
||||
selects that exact result without reading or rescanning either version input.
|
||||
The workspace still requires read authority. REA rejects a selector that is
|
||||
missing, incomplete, or inconsistent with the requested paths and controls.
|
||||
|
||||
## MCP
|
||||
|
||||
`find_changed_behavior` retains its existing aggregation mode. Its additive
|
||||
`investigation_run` mode starts or resumes the same persistent workflow:
|
||||
|
||||
```json
|
||||
{
|
||||
"investigation_run": {
|
||||
"approved": true,
|
||||
"workspace_path": "/absolute/path/to/evidence/releases.json",
|
||||
"workspace_name": "releases",
|
||||
"left_path": "/absolute/path/to/version-1",
|
||||
"right_path": "/absolute/path/to/version-2",
|
||||
"options": {
|
||||
"max_entries": 10000,
|
||||
"max_total_bytes": 1073741824,
|
||||
"max_entry_bytes": 268435456,
|
||||
"max_compression_ratio": 1000,
|
||||
"max_depth": 20,
|
||||
"max_path_bytes": 4096,
|
||||
"page_size": 500,
|
||||
"change_limit": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Exactly one of `comparisons` or `investigation_run` is accepted. The MCP tool
|
||||
inventory remains unchanged.
|
||||
|
||||
## Checkpoints and reuse
|
||||
|
||||
One run has four ordered stages:
|
||||
|
||||
1. Inventory the left version.
|
||||
2. Inventory the right version.
|
||||
3. Compare the complete artifact Evidence page sets.
|
||||
4. Derive the changed-behavior report.
|
||||
|
||||
The inventory checkpoint is written first, followed by the comparison and final
|
||||
report checkpoints. A process interrupted after a checkpoint can resume from
|
||||
the last complete stage. A completed run is reused when both graph commitments
|
||||
and every bounded option are identical.
|
||||
|
||||
By default, REA rescans the version inputs so changed content creates a new
|
||||
run. For an explicit cache-only replay, add the returned `run_id` as
|
||||
`investigation_run.replay_run_id`. REA validates the workspace revision and
|
||||
name, the complete run, its exact paths and controls, and every linked Evidence
|
||||
record before it skips input-read authorization.
|
||||
|
||||
The run ID is a SHA-256 commitment to both root and graph digests plus the
|
||||
normalized options. Local paths and timestamps do not affect that identity.
|
||||
Changing either artifact or a budget creates a new run in the same workspace;
|
||||
it does not overwrite the earlier run.
|
||||
|
||||
## Integrity and concurrency
|
||||
|
||||
A workspace contains a canonical Evidence v2 bundle plus run manifests. Each
|
||||
workspace revision has a deterministic digest and points to the prior revision
|
||||
digest. Parsing verifies:
|
||||
|
||||
- the workspace, run, Evidence, graph, and revision identities;
|
||||
- canonical ordering and unique run IDs;
|
||||
- every run-to-Evidence reference;
|
||||
- stage and completion-state consistency.
|
||||
|
||||
Writes are canonical JSON, atomic, fsynced, and mode `0600`. REA rejects
|
||||
workspace paths outside `REA_EVIDENCE_ROOTS_JSON`, symlink destinations,
|
||||
artifact inputs outside `REA_INVESTIGATION_INPUT_ROOTS_JSON`,
|
||||
oversized or deeply nested JSON, stale expected revisions, and concurrent lock
|
||||
holders. Lock or CAS conflicts fail without mutating the workspace; callers may
|
||||
retry the same idempotent request.
|
||||
|
||||
## Truth boundary
|
||||
|
||||
The automatic run currently observes shipped artifact structure. Artifact
|
||||
changes are static behavior candidates, not runtime observations or causal
|
||||
claims. Without controlled process comparison Evidence, `behavior_status`
|
||||
remains `unknown` even when static differences are present. Provider
|
||||
limitations, incomplete comparison pages, and unsupported container expansion
|
||||
remain explicit in the final Evidence.
|
||||
|
||||
Automatic Hopper function matching, process replay, protocols, UI comparison,
|
||||
and reconstruction verification are not yet stages of this run. Existing
|
||||
manual comparison Evidence can still be aggregated through the original
|
||||
`find_changed_behavior` mode.
|
||||
@@ -1,6 +1,6 @@
|
||||
# JavaScript Application Graph
|
||||
|
||||
JavaScript Application Graph v1 is REA's provider-neutral domain model for
|
||||
JavaScript Application Graph is REA's provider-neutral domain model for
|
||||
connecting shipped application bytes, JavaScript structure, Electron process
|
||||
boundaries, passive browser observations, and native add-ons. It is a durable,
|
||||
canonical data contract; it is not an extractor or a new CLI/MCP tool.
|
||||
@@ -32,13 +32,12 @@ adds capture-scoped targets, frames, scripts, workers, and inferred
|
||||
verification remains a separate later layer. The domain graph itself never
|
||||
reaches outward to obtain any of those facts.
|
||||
|
||||
## Versioned record
|
||||
## Stored record
|
||||
|
||||
A stored graph has this top-level shape:
|
||||
|
||||
```text
|
||||
schema: "JavaScriptApplicationGraph"
|
||||
schema_version: 1
|
||||
graph_id: jag_<sha256 of normalized graph semantics>
|
||||
root_node_ids: jag_node_<sha256>[]
|
||||
nodes: application entities with one or more observations
|
||||
@@ -47,14 +46,14 @@ coverage: complete, partial, unknown, or unavailable
|
||||
limitations: explicit graph-wide constraints
|
||||
```
|
||||
|
||||
Parsing rejects unknown fields, unsupported versions, non-canonical ordering,
|
||||
duplicate IDs, stale semantic commitments, missing roots, dangling endpoints,
|
||||
self-edges, and `changed_from` edges between unlike node kinds. Constructors
|
||||
normalize set-like arrays before deriving their IDs.
|
||||
Parsing rejects unknown fields, non-canonical ordering, duplicate IDs, stale
|
||||
semantic commitments, missing roots, dangling endpoints, self-edges, and
|
||||
`changed_from` edges between unlike node kinds. Constructors normalize set-like
|
||||
arrays before deriving their IDs.
|
||||
|
||||
## Entities and relationships
|
||||
|
||||
The v1 node vocabulary covers:
|
||||
The node vocabulary covers:
|
||||
|
||||
- package, installer, and artifact;
|
||||
- ASAR entry;
|
||||
@@ -107,7 +106,7 @@ Every observation and edge carries all of the following:
|
||||
location, or an unavailable reason;
|
||||
- extractor name, version, operation, and optional executable digest;
|
||||
- coverage status, truncation, omitted count, and named limits;
|
||||
- limitations and links to existing Evidence v2 records;
|
||||
- limitations and links to existing Evidence records;
|
||||
- a content-derived identifier and its declared stability strategy.
|
||||
|
||||
Authority and epistemic state are separate fields. An AST observation does not
|
||||
@@ -158,16 +157,12 @@ stability. Edge IDs use the same derivation with `relationship-exact`
|
||||
stability. Node, observation, edge, and graph IDs are recomputed during parsing,
|
||||
so callers cannot preserve a stale ID after changing semantic content.
|
||||
|
||||
## Bounds
|
||||
## Structure
|
||||
|
||||
The v1 boundary admits at most 1,000 roots, 100,000 nodes, 200,000 edges, and 64
|
||||
observations per node. Each properties object is valid bounded JSON with at most
|
||||
64 keys, depth 6, 512 structural nodes, and 4,096 characters per string. Arrays
|
||||
of evidence links, limits, and limitations have independent caps.
|
||||
|
||||
These are admission limits, not completeness claims. When an extractor reaches
|
||||
a limit, it must record partial coverage and the exact named limit. It must not
|
||||
return a bounded prefix labeled complete.
|
||||
The graph does not impose aggregate caps on roots, nodes, edges, observations,
|
||||
or JSON properties. Properties must be valid JSON; field-specific validation
|
||||
still applies to paths, identifiers, and evidence values. Extractors report
|
||||
actual omissions in coverage instead of silently dropping a prefix.
|
||||
|
||||
## Domain API
|
||||
|
||||
@@ -184,7 +179,3 @@ The public domain functions are:
|
||||
- `serializeJavaScriptApplicationGraph` for canonical JSON;
|
||||
- `computeJavaScriptApplicationGraphSha256` for a byte-stable verified graph
|
||||
digest.
|
||||
|
||||
Schema evolution is explicit. A future incompatible shape receives a new
|
||||
`schema_version`; v1 parsing fails with an unsupported-version diagnostic rather
|
||||
than guessing how to reinterpret it.
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
# Cross-layer JavaScript application workflows
|
||||
|
||||
REA derives a bounded feature trace from one authenticated JavaScript
|
||||
REA derives a complete reachable feature trace from one authenticated JavaScript
|
||||
Application Graph and compares two authenticated graph versions. The MCP tools
|
||||
are `trace_application_feature`, `trace_javascript_semantics`,
|
||||
`compare_application_versions`, `compare_source_to_bundle`, and
|
||||
`compare_javascript_export_shapes`; their CLI equivalents use the same names
|
||||
with hyphens.
|
||||
|
||||
Both workflows consume Evidence v2 produced by
|
||||
Both workflows consume Evidence produced by
|
||||
`analyze_javascript_application` or `reconcile_javascript_runtime`. They do not
|
||||
read an artifact, execute application code, attach to a process, or open a
|
||||
native-analysis provider. Static artifact observations, passive runtime
|
||||
observations, relationship inferences, and unknown or unavailable facts retain
|
||||
their original graph authority.
|
||||
|
||||
Current `analyze_javascript_application` Evidence uses result schema v2. It
|
||||
retains the structural JavaScript Application Graph and a separate semantic
|
||||
relation graph bound to the same root artifact digest and structural graph ID.
|
||||
Legacy v1 Evidence remains valid for structural workflows, but semantic tracing
|
||||
returns an actionable request to reanalyze it rather than treating missing
|
||||
semantic data as an empty graph.
|
||||
`analyze_javascript_application` Evidence retains the structural JavaScript
|
||||
Application Graph and a separate semantic relation graph bound to the same root
|
||||
artifact digest and structural graph ID. Semantic tracing requires the semantic
|
||||
relation graph and reports when it is unavailable rather than treating missing
|
||||
data as an empty graph.
|
||||
|
||||
## Feature tracing
|
||||
|
||||
Select one literal seed kind: node ID, route, string, API, IPC channel, module,
|
||||
or native export. Matching is exact or literal substring matching; regular
|
||||
expressions and executable predicates are not accepted. Direction, depth,
|
||||
nodes, edges, paths, and seed matches all have caller-visible limits.
|
||||
expressions and executable predicates are not accepted. The trace includes all
|
||||
matching seeds and the complete graph reachable in the selected direction.
|
||||
|
||||
The result contains the matching basis, a bounded subgraph, terminal paths,
|
||||
authority summaries, truncation frontier, and native handoffs. A handoff binds
|
||||
The result contains the matching basis, complete reachable subgraph, terminal
|
||||
paths, authority summaries, and native handoffs. A handoff binds
|
||||
the exact native artifact digest and requested exports from the application
|
||||
graph. Existing Hopper or Ghidra Evidence is linked only when its subject digest
|
||||
matches exactly. Otherwise the result recommends provider-neutral follow-up
|
||||
@@ -42,16 +41,15 @@ provider implicitly.
|
||||
rereading mutable files. A seed may identify a semantic or application node,
|
||||
literal, function fingerprint, property, endpoint, event, or boundary field.
|
||||
The query declares backward provenance, forward influence, callers, or
|
||||
ownership and can restrict admitted relation kinds. Node, relation, depth,
|
||||
function, module, seed-match, and page limits are validated against exact
|
||||
published ranges. A committed cursor returns the next deterministic relation
|
||||
page only for the same graph, query, and limits.
|
||||
ownership and can restrict admitted relation kinds. REA returns every matching
|
||||
seed and the complete reachable subgraph; callers do not choose node, relation,
|
||||
depth, function, module, seed-match, or page budgets.
|
||||
|
||||
The extractor covers lexical definitions and reads, literal seeds, static
|
||||
property slots, object reads/writes/spreads/destructuring, closure captures,
|
||||
uniquely resolved local calls, argument/return flow, and explicit Promise
|
||||
construction, chaining, aggregation, await, return, assignment, and
|
||||
detachment. It also recovers bounded static candidates for EventEmitter
|
||||
detachment. It also recovers static candidates for EventEmitter
|
||||
registrations/removals and dispatch, Node timers and cancellation handles, asynchronous
|
||||
`node:child_process` creation and ownership, configuration sources and direct
|
||||
defaults, request construction and response consumers, parse/coercion/
|
||||
@@ -60,9 +58,9 @@ validation boundaries, and built-in resource acquisition/release.
|
||||
Function fingerprints commit normalized syntax, control-flow shape, relation
|
||||
shape, literal sets, arity, and detected effects without using local names or
|
||||
source offsets. Equal duplicate fingerprints remain ambiguous. Dynamic
|
||||
calls/properties/names, parser recovery, unresolved handles, unsupported API
|
||||
shapes, and every reached bound stay explicit unknowns; a missing relation is
|
||||
never reported as proven behavioral absence.
|
||||
calls/properties/names, parser recovery, unresolved handles, and unsupported API
|
||||
shapes stay explicit unknowns; a missing relation is never reported as proven
|
||||
behavioral absence.
|
||||
|
||||
Semantic relations use static-inference authority. `resolved` means the static
|
||||
target identity was unique within admitted analysis; it does not mean the path
|
||||
@@ -83,19 +81,21 @@ persistent identities. Duplicate candidates remain ambiguous. Source-map and
|
||||
structural matches are high/medium-confidence inferences rather than exact
|
||||
facts. Each item is `unchanged`, `added`, `removed`, `changed`, or `unknown`,
|
||||
and the result includes its basis, candidates, changed dimensions, Evidence
|
||||
links, limitations, and a bounded `changed_from` graph.
|
||||
links, limitations, and a `changed_from` graph containing all compared nodes,
|
||||
their observations, and relationships.
|
||||
|
||||
One-sided absence is `added` or `removed` only when the opposite input graph has
|
||||
complete coverage. With partial, unavailable, or truncated input, the same
|
||||
condition is `unknown`. Output truncation separately reports omitted comparison
|
||||
items, candidate references, graph nodes, edges, and observations. With
|
||||
`unknown_registry_approved: true`, unresolved comparison items can be retained
|
||||
as a residual unknown in a live session.
|
||||
condition is `unknown`. The comparison retains every classified item and
|
||||
ambiguity candidate. Coverage carries omitted counts and limits from each input
|
||||
graph; comparison adds no caller-selected item, candidate, graph node, or edge
|
||||
budget. Unresolved comparison items are recorded as a residual unknown linked
|
||||
to the comparison Evidence in a live session.
|
||||
|
||||
## Historical source-to-bundle comparison
|
||||
|
||||
`compare_source_to_bundle` compares one cryptographically committed
|
||||
`HistoricalSourceGraph/v1` with authenticated application-graph Evidence. The
|
||||
`HistoricalSourceGraph` with authenticated application-graph Evidence. The
|
||||
stable scoring model reports every admitted signal and weight: exact source
|
||||
digest, source-map original path, exact current path, path suffix, basename, and
|
||||
language extension. Exact digest wins over location inference; weak basename
|
||||
@@ -103,12 +103,13 @@ or extension evidence never forces a mapping.
|
||||
|
||||
Each historical source file is classified as `unchanged`, `modified`,
|
||||
`removed`, `split`, `merged`, `duplicated`, or `unknown`. Multiple equally
|
||||
scored path mappings form a bounded static split inference. Multiple current
|
||||
scored path mappings form a static split inference. Multiple current
|
||||
nodes with the exact historical digest are duplicated; multiple historical
|
||||
paths with exact identity to one current node are merged. `removed` requires
|
||||
complete historical and application inventories with no relevant comparison
|
||||
truncation. Ambiguous candidates, partial inventories, and exhausted limits
|
||||
remain unknown. These static mappings do not claim runtime loading or semantic
|
||||
complete historical and application inventories. The comparison analyzes and
|
||||
returns all source files, candidate nodes, and matching signals represented in
|
||||
the supplied graphs. Partial inventories remain unknown where absence cannot
|
||||
be established. These static mappings do not claim runtime loading or semantic
|
||||
equivalence.
|
||||
|
||||
## Export return-shape comparison
|
||||
@@ -120,9 +121,9 @@ match. An export is linked to a callable only through exact lexical/module
|
||||
relationships recovered by the AST analysis.
|
||||
|
||||
Direct return expressions, including expression-bodied arrows, are evaluated
|
||||
through a bounded execution-free value lattice. Literal object fields are
|
||||
static observations. Calls, dynamic spreads, computed keys, parser recovery,
|
||||
and exhausted value, property, or return-site limits remain partial or unknown.
|
||||
through an execution-free value lattice. Literal object fields and direct
|
||||
return sites are represented in the result. Calls, dynamic spreads, computed
|
||||
keys, and parser recovery remain partial or unknown.
|
||||
Nested callable returns are not assigned to their parent callable. Projected
|
||||
graph observations carry source ranges but never source text.
|
||||
|
||||
@@ -136,8 +137,9 @@ replay recommendation; it does not execute JavaScript.
|
||||
|
||||
## CLI and verification
|
||||
|
||||
All five CLI commands accept inline JSON or a JSON file up to 64 MiB. The input
|
||||
is the same object used by the corresponding MCP tool.
|
||||
All five CLI commands accept inline JSON or a path to a JSON file. Put the full
|
||||
Evidence records in the workflow input, as in the corresponding MCP tool; the
|
||||
CLI does not resolve Evidence IDs from a separate bundle.
|
||||
|
||||
For two operator-provided directories or ASARs, run:
|
||||
|
||||
@@ -158,8 +160,8 @@ all four `--left-module-path`, `--left-export-name`, `--right-module-path`, and
|
||||
`--right-export-name` options to verify an explicit export pair.
|
||||
|
||||
Because source Evidence and derived Evidence are retained by the normal session
|
||||
ledger, evidence bundles, analysis snapshots, and investigation workspaces can
|
||||
carry these records without another persistence format.
|
||||
ledger, evidence bundles and analysis snapshots can carry these records without
|
||||
another persistence format.
|
||||
|
||||
## Controlled replay boundary
|
||||
|
||||
@@ -175,3 +177,9 @@ Replay observations retain `controlled-replay` authority. They cannot
|
||||
promote static inference into passive runtime observation or prove that the
|
||||
original application, renderer, preload, main process, or remote service
|
||||
behaved identically.
|
||||
|
||||
Module, explicit case, and preset-generated case lists have no fixed
|
||||
item-count ceiling. Replay plans commit the caller's resource budgets, and the
|
||||
worker protocol checks the complete request against that declared byte budget
|
||||
before probing executables or reading modules. Aggregate module-source and
|
||||
case-input bytes are likewise checked against the caller's declared budgets.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# JavaScript artifact reconstruction
|
||||
|
||||
REA can project an operator-supplied Electron/JavaScript application directory
|
||||
or ASAR into [JavaScript Application Graph v1](javascript-application-graph.md)
|
||||
or ASAR into [JavaScript Application Graph](javascript-application-graph.md)
|
||||
without executing application code. The target-free
|
||||
`analyze_javascript_application` MCP tool, dedicated
|
||||
`rea analyze-javascript-application` CLI command, and generic
|
||||
`rea analyze PATH --approved` directory/ASAR route expose the same application
|
||||
service and return an Evidence v2 envelope containing the graph and an Electron
|
||||
`rea analyze PATH` directory/ASAR route expose the same application
|
||||
service and return an Evidence envelope containing the graph and an Electron
|
||||
boundary summary.
|
||||
|
||||
The resulting Evidence can be combined with passive web or Electron capture
|
||||
@@ -17,50 +17,38 @@ separate static, runtime, and cross-layer inference authorities.
|
||||
|
||||
## Public workflow
|
||||
|
||||
The administrator first grants a canonical input root. Every request still
|
||||
requires explicit per-call approval, and the requested ASAR or directory must
|
||||
remain beneath that root after canonicalization:
|
||||
The tool reads the caller-selected local directory or ASAR path directly:
|
||||
|
||||
```bash
|
||||
export REA_INVESTIGATION_INPUT_ROOTS_JSON='["/absolute/path/to/apps"]'
|
||||
rea analyze /absolute/path/to/apps/app.asar --approved --json
|
||||
rea analyze /absolute/path/to/apps/app.asar --json
|
||||
rea analyze-javascript-application /absolute/path/to/apps/app.asar \
|
||||
--approved \
|
||||
--json
|
||||
```
|
||||
|
||||
Generic `rea analyze` selects this static provider for directories and `.asar`
|
||||
paths when neither `--provider` nor `--snapshot` is supplied. Use the dedicated
|
||||
command when setting explicit format or reconstruction limits. Native targets,
|
||||
single JavaScript files, `.app` bundles, and explicit deep-provider or snapshot
|
||||
requests retain the native deep-analysis route.
|
||||
paths when neither `--provider` nor `--snapshot` is supplied. The dedicated
|
||||
command and the generic route return their complete results inline. Native
|
||||
targets, single JavaScript files, `.app` bundles, and explicit deep-provider or
|
||||
snapshot requests retain the native deep-analysis route.
|
||||
|
||||
When configuring an MCP client, set the variable before approved setup so REA
|
||||
copies the explicit non-secret root policy into the managed registration:
|
||||
Configure an MCP client with the ordinary REA setup command:
|
||||
|
||||
```bash
|
||||
export REA_INVESTIGATION_INPUT_ROOTS_JSON='["/absolute/path/to/apps"]'
|
||||
rea setup
|
||||
```
|
||||
|
||||
Restart the configured MCP client after changing this administrator ceiling.
|
||||
An already-running server cannot acquire a new process environment through
|
||||
MCP elicitation or `SIGHUP`.
|
||||
|
||||
The equivalent MCP input is:
|
||||
|
||||
```json
|
||||
{
|
||||
"input_path": "/absolute/path/to/apps/app.asar",
|
||||
"format": "auto",
|
||||
"approved": true,
|
||||
"source_map_read_approved": false
|
||||
"format": "auto"
|
||||
}
|
||||
```
|
||||
|
||||
`input_path` must be absolute. `format` accepts `auto`, `asar`, or `directory`.
|
||||
The result retains the canonical local path, root artifact digest, artifact
|
||||
manifest and graph commitments, JavaScript Application Graph v1, static
|
||||
manifest and graph commitments, JavaScript Application Graph, static
|
||||
Electron summary, reconstruction statistics, and explicit limitations. It does
|
||||
not require a live Hopper, Ghidra, browser, or Electron process.
|
||||
|
||||
@@ -90,7 +78,7 @@ Selected bounded text is then parsed as inert data to recover:
|
||||
- static imports, dynamic imports, CommonJS `require` calls, workers, and
|
||||
service workers;
|
||||
- route, network endpoint, storage, and vendor-marker observations;
|
||||
- local source-map declarations and, when separately approved, bounded original
|
||||
- local source-map declarations and original
|
||||
source names and content digests;
|
||||
- explicit BrowserWindow options and `webPreferences`, including statically
|
||||
resolvable preload entrypoints;
|
||||
@@ -119,11 +107,7 @@ JavaScript file, package record, or source map produces an `unknown` graph scope
|
||||
with `state: unavailable`; it is not treated as evidence that modules or source
|
||||
are absent.
|
||||
|
||||
Source-map content has a separate `source_map_read_approved` input. Without that
|
||||
approval, REA inventories the map, links a static declaration when present, and
|
||||
records partial non-truncated coverage plus an unavailable parse scope. With
|
||||
approval, the graph stores original source names and optional content digests,
|
||||
not the raw `sourcesContent` text.
|
||||
Source-map contents are parsed as local input. The graph stores original source names and optional content digests, not raw `sourcesContent` text.
|
||||
|
||||
Only explicitly present BrowserWindow values are observations. REA does not
|
||||
substitute version-dependent Electron defaults for omitted `webPreferences`.
|
||||
@@ -160,30 +144,29 @@ represented by metadata only.
|
||||
|
||||
## Bounds and coverage
|
||||
|
||||
The input schema bounds artifact entries, cumulative artifact bytes, bytes per
|
||||
entry, compression ratio, path depth and length, selected text files, text bytes,
|
||||
AST nodes, static findings, bundle modules, source-map originals, and cooperative
|
||||
parse time. Source-map original limits are shared across all maps in one
|
||||
reconstruction. Combined input limits are rejected if their conservative graph
|
||||
projection could exceed the v1 contract's 100,000 nodes or 200,000 edges.
|
||||
The reconstruction keeps internal resource safeguards for artifact entries,
|
||||
cumulative artifact bytes, bytes per entry, compression ratio, path depth and
|
||||
length, selected text files, text bytes, AST nodes, and cooperative parse time.
|
||||
These safeguards are not caller-selectable output budgets. Source-map parsing
|
||||
reports truncation when its format or parser safety boundary is reached. The
|
||||
application graph has no aggregate node, edge, root, or observation prefix cap.
|
||||
|
||||
Byte, entry, path, and graph-shape bounds are hard limits. The parse deadline is
|
||||
checked before and between bounded parsing and traversal phases; the synchronous
|
||||
Babel and JSON parser calls cannot be preempted mid-call, so their input byte
|
||||
bounds remain the hard protection for an individual call.
|
||||
|
||||
Byte-identical assets share a content-digest node. At most 64 distinct
|
||||
observations are retained on that node, as required by the graph contract; every
|
||||
inventoried containment edge remains present, and any omitted observations make
|
||||
top-level coverage partial and truncated. When an exact omission count is not
|
||||
knowable, `omitted_count` is `null` rather than a guessed value.
|
||||
Byte-identical assets share a content-digest node. Every distinct observation
|
||||
and inventoried containment edge is retained. If parsing or artifact safety
|
||||
boundaries omit input, coverage reports that omission; when an exact omission
|
||||
count is not knowable, `omitted_count` is `null` rather than a guessed value.
|
||||
|
||||
## Verification boundary
|
||||
|
||||
The source-owned fixture covers an extracted directory, a direct ASAR with an
|
||||
unpacked native add-on, a direct ASAR whose unpacked native companion is
|
||||
missing, and an ASAR nested beneath a directory. Tests also cover deterministic
|
||||
reruns, parse-not-execute behavior, source-map approval, global source-map
|
||||
reruns, parse-not-execute behavior, source-map parsing, global source-map
|
||||
limits, malformed structured data, invalid containers, traversal, symlink
|
||||
escape, oversized text, cancellation, AST truncation, and repeated content
|
||||
identities. A separate synthetic Electron fixture covers explicit safe and
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Passive Node and Electron runtime observation
|
||||
|
||||
REA can attach to an already-running Node.js or Electron V8 Inspector target
|
||||
and retain bounded script-load and execution-context metadata. The
|
||||
and retain script-load and execution-context metadata. The
|
||||
`list_javascript_runtime_targets` and `observe_javascript_runtime` MCP tools
|
||||
have equivalent `rea list-javascript-runtime-targets` and
|
||||
`rea observe-javascript-runtime` commands. Successful calls return
|
||||
deterministic Evidence v2.
|
||||
deterministic Evidence.
|
||||
|
||||
This authority is disabled by default and is separate from browser CDP,
|
||||
Electron file-page inspection, Process Capture, and controlled JavaScript
|
||||
@@ -13,23 +13,19 @@ replay:
|
||||
|
||||
```bash
|
||||
export REA_V8_INSPECTOR_OBSERVE_ENABLED=true
|
||||
export REA_V8_INSPECTOR_ENDPOINTS_JSON='["http://127.0.0.1:9229"]'
|
||||
export REA_V8_INSPECTOR_FILE_ROOTS_JSON='["/absolute/path/to/app"]'
|
||||
export REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON='[]'
|
||||
|
||||
rea list-javascript-runtime-targets http://127.0.0.1:9229 \
|
||||
--approved --json
|
||||
--json
|
||||
rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
|
||||
--runtime-kind node --approved --json
|
||||
--runtime-kind node --json
|
||||
```
|
||||
|
||||
At least one canonical file root or exact HTTP(S) origin is required.
|
||||
Discovery is restricted to a literal-loopback HTTP endpoint. A target must
|
||||
match the exact requested target ID, expose a same-port WebSocket reported by
|
||||
the approved discovery endpoint, and remain inside the requested file or
|
||||
origin scope. Targets already marked attached are rejected rather than
|
||||
displacing another debugger. Canonical file paths are checked after symlink resolution.
|
||||
Excluded locations are counted but never retained.
|
||||
The caller supplies the literal-loopback HTTP endpoint directly. Selecting an endpoint exposes all targets it serves and their local script locations or renderer origins. A target must match the exact requested target ID and expose a same-port WebSocket reported by that endpoint. Targets already marked attached are rejected rather than displacing another debugger. Local paths are canonicalized after symlink resolution.
|
||||
|
||||
`list_javascript_runtime_targets` returns every available target in one inline
|
||||
array.
|
||||
|
||||
For the MCP follow-up call, pass the same literal-loopback endpoint and the selected target ID to `observe_javascript_runtime`. REA rediscovers targets and validates the ID against the live endpoint before observation.
|
||||
|
||||
## Passive protocol boundary
|
||||
|
||||
@@ -73,20 +69,19 @@ Electron main, preload, and renderer behavior can therefore be observed only
|
||||
as separate approved Inspector targets. The provider does not infer that two
|
||||
targets belong to the same Electron application.
|
||||
|
||||
## Bounds and determinism
|
||||
## Capture and determinism
|
||||
|
||||
Every observation commits the window and exact limits for events, scripts,
|
||||
execution contexts, per-location bytes, and total retained metadata bytes.
|
||||
Limit hits increment dropped-event counts and set `capture.truncated`; they
|
||||
never become completeness claims. Scripts are authorized after capture,
|
||||
deduplicated by stable metadata, and canonically sorted. Wall-clock timestamps
|
||||
and protocol script IDs are excluded from the durable result, so identical
|
||||
inputs and captured metadata produce the same Evidence ID.
|
||||
Every observation records its time window and returns all valid script and
|
||||
execution-context events received during it. Per-location protocol validation
|
||||
remains in force. Scripts are authorized after capture, deduplicated by stable
|
||||
metadata, and canonically sorted. Wall-clock timestamps and protocol script IDs
|
||||
are excluded from the durable result, so identical inputs and captured metadata
|
||||
produce the same Evidence ID.
|
||||
|
||||
Inspector attach is inherently incomplete. Enabling `Debugger` reports known
|
||||
and uncollected scripts, but scripts collected before attachment may be
|
||||
missing. Absence from a bounded capture never proves that a script or behavior
|
||||
does not occur.
|
||||
missing. Absence from the observation window never proves that a script or
|
||||
behavior does not occur.
|
||||
|
||||
## Static correlation
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
REA can combine an existing static JavaScript Application Graph with one or
|
||||
more passive browser, Electron page, or Node/Electron V8 Inspector captures without collapsing their different
|
||||
authorities. The `reconcile_javascript_runtime` MCP tool and
|
||||
`rea reconcile-javascript-runtime` CLI command accept Evidence v2 records and
|
||||
return derived Evidence v2 containing a combined JavaScript Application Graph
|
||||
`rea reconcile-javascript-runtime` CLI command accept Evidence records and
|
||||
return derived Evidence containing a combined JavaScript Application Graph
|
||||
and explicit match, ambiguity, mismatch, and unknown classifications.
|
||||
|
||||
The operation performs no filesystem, browser, or Electron I/O. It accepts only
|
||||
semantically verified Evidence produced by `analyze_javascript_application`,
|
||||
`inspect_web_page`, `inspect_electron_page`, or
|
||||
`observe_javascript_runtime`; matching operation names alone
|
||||
is insufficient. Provider identity, predicate version, subject digest, target
|
||||
is insufficient. Provider identity, predicate type, subject digest, target
|
||||
parameters, and normalized-result schemas are checked before reconciliation.
|
||||
|
||||
## Workflow
|
||||
@@ -30,7 +30,7 @@ parameters, and normalized-result schemas are checked before reconciliation.
|
||||
and asset roots need explicit mappings.
|
||||
|
||||
The MCP input has this shape; the `analysis` and `runtime_observations` values
|
||||
are complete Evidence v2 objects, not only evidence IDs:
|
||||
are complete Evidence objects, not only evidence IDs:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -47,12 +47,7 @@ are complete Evidence v2 objects, not only evidence IDs:
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtime_observations": ["INSPECT_WEB_PAGE_EVIDENCE"],
|
||||
"limits": {
|
||||
"max_runtime_entities": 10000,
|
||||
"max_reconciliation_items": 20000,
|
||||
"max_static_load_states": 20000
|
||||
}
|
||||
"runtime_observations": ["INSPECT_WEB_PAGE_EVIDENCE"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -101,13 +96,13 @@ nodes. REA evaluates compatible static candidates conservatively:
|
||||
3. Without captured bytes, one unique mapped artifact path can produce a
|
||||
medium-confidence location match.
|
||||
4. Multiple candidates remain `ambiguous`; no `observed_as` edge is emitted.
|
||||
`candidate_static_count` reports the full qualified layer/node count while
|
||||
`candidate_static_nodes` retains a deterministic bounded prefix.
|
||||
`candidate_static_count` reports the full qualified layer/node count, and
|
||||
`candidate_static_nodes` includes every unique candidate reference.
|
||||
5. If captured bytes disagree with the static candidate at the mapped path,
|
||||
the result is an explicit mismatch. A path never overrides a digest
|
||||
disagreement.
|
||||
6. Missing authorization mappings, incomplete input coverage, and output
|
||||
limits remain unknown or truncated rather than becoming absence claims.
|
||||
6. Missing authorization mappings and incomplete or truncated input coverage
|
||||
remain unknown or truncated rather than becoming absence claims.
|
||||
|
||||
Every successful cross-layer `observed_as` edge has
|
||||
`authority: cross-layer-reconciliation` and `state: inferred`. The underlying
|
||||
@@ -124,7 +119,7 @@ classification:
|
||||
- `resident-in-loaded-asset` means a containing asset was observed, but the
|
||||
embedded module was not independently observed or shown to execute;
|
||||
- `not-observed-in-capture` means the entity did not appear in the complete
|
||||
bounded captures supplied to this call and its artifact path falls beneath
|
||||
authorized captures supplied to this call and its artifact path falls beneath
|
||||
an exercised automatic or operator-declared mapping prefix;
|
||||
- `unknown` covers incomplete captures, layers outside runtime scope, and
|
||||
unresolved correspondence. Nodes outside a mapped artifact prefix stay
|
||||
@@ -156,18 +151,18 @@ silently equated with the generated bytes observed by CDP.
|
||||
|
||||
## Bounds and determinism
|
||||
|
||||
Inputs allow up to eight static layers and 32 passive captures. Default output
|
||||
bounds retain 10,000 runtime entities, 20,000 reconciliation records, and
|
||||
20,000 static load states; each caller-visible maximum is 50,000. At least one
|
||||
target node per capture must fit. The combined graph also preserves the JAG v1
|
||||
caps of 1,000 roots, 100,000 nodes, 200,000 edges, and 64 observations per
|
||||
node. Reaching a limit reports exact omitted counts for runtime entities,
|
||||
reconciliation items, static load states, and merged graph items, plus
|
||||
`coverage.status: truncated`.
|
||||
The workflow does not impose aggregate count caps on static layers, passive
|
||||
captures, runtime entities, reconciliation items, static load states, runtime
|
||||
mapping declarations, or candidate references. These collections remain
|
||||
subject to the structural validation applied to each Evidence record and
|
||||
application graph; passive observation returns all in-window collection items
|
||||
that pass per-value validation. Reconciliation reports zero
|
||||
omitted items when it projects the complete supplied inputs. Static JavaScript
|
||||
analysis can still report its own bounded AST coverage as partial or truncated.
|
||||
|
||||
Layers, captures, nodes, edges, matches, and evidence links are canonically
|
||||
ordered. Repeating the same verified Evidence, mappings, and limits produces
|
||||
the same reconciliation, graph, and Evidence identifiers.
|
||||
ordered. Repeating the same verified Evidence and mappings produces the same
|
||||
reconciliation, graph, and Evidence identifiers.
|
||||
|
||||
## Non-goals
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ This document turns
|
||||
[ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md) into an
|
||||
implementation and verification plan. REA currently ships read-only PE/CLI
|
||||
triage and exact identity through `inspect_managed_artifact` and
|
||||
`rea inspect-managed-artifact`, plus bounded metadata, signature, method-body
|
||||
CIL, exception-region, call-edge, and field-access inspection through
|
||||
`inspect_managed_members` and `rea inspect-managed-members`, and
|
||||
`rea inspect-managed-artifact`, plus file-backed metadata, signature,
|
||||
method-body CIL, exception-region, call-edge, and field-access inspection
|
||||
through `inspect_managed_members` and `rea inspect-managed-members`, and
|
||||
declared ModuleRef/ImplMap/PInvoke and native implementation boundary inventory
|
||||
through `inspect_managed_native_boundaries` and
|
||||
`rea inspect-managed-native-boundaries`, plus obfuscation-resistant member
|
||||
@@ -91,7 +91,7 @@ commitment groups:
|
||||
- module name, generation, and MVID;
|
||||
- target framework and runtime/version strings with their exact metadata
|
||||
locations;
|
||||
- parser/provider version, profile schema, limits, and profile digest.
|
||||
- provider identity, analysis-affecting parameters, and their profile digest.
|
||||
|
||||
CLI `#GUID` heap values are committed as their exact 16-byte GUID text. REA
|
||||
does not impose RFC 4122 UUID version or variant bits on MVID, EncId, or
|
||||
@@ -106,23 +106,22 @@ EncBaseId because ECMA-335 metadata does not require those bit patterns.
|
||||
- exact raw bytes or bounded value digest;
|
||||
- raw CIL digest plus separately reported method-header, locals-token, and
|
||||
exception-region observations where available;
|
||||
- a deterministic v1 decoded-instruction-tuple digest for completely decoded
|
||||
- a deterministic decoded-instruction-tuple digest for completely decoded
|
||||
CIL, with the limitations below.
|
||||
|
||||
### Shipped decoded-CIL fingerprint v1
|
||||
### Shipped decoded-CIL fingerprint
|
||||
|
||||
`inspect_managed_members` exposes two method CIL hashes with different byte
|
||||
boundaries:
|
||||
|
||||
| Field | Shipped v1 input | Excluded from the digest |
|
||||
| Field | Input | Excluded from the digest |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `il_sha256` | The exact `il_size` raw CIL bytes after the tiny or fat method header | Method header, locals signature blob, alignment, and extra sections including exception clauses |
|
||||
| `normalized_il_sha256` | UTF-8 `JSON.stringify` output for the instruction-order array of `[opcode, operand_kind, operand]` tuples | Instruction offsets except projected scalar branch targets, switch targets, resolved metadata/string identities, method header, locals, and exception regions |
|
||||
|
||||
The normalized field is available only when the entire CIL stream decodes with
|
||||
status `present`. A partial, malformed, absent, or over-limit body reports
|
||||
`null`. The containing member result's `schema_version: 1` scopes the tuple
|
||||
format; v1 does not expose a separate algorithm-version field.
|
||||
status `present`. A malformed or absent body reports `null`. The tuple format
|
||||
is defined here.
|
||||
|
||||
Tuple operands use these exact projections:
|
||||
|
||||
@@ -134,17 +133,17 @@ Tuple operands use these exact projections:
|
||||
- signed integer and variable operands: decimal strings; floating-point
|
||||
operands: JavaScript's `String` representation of the decoded IEEE value.
|
||||
|
||||
Opcode names retain short/long forms and prefix opcodes. This makes v1 a
|
||||
Opcode names retain short/long forms and prefix opcodes. This makes the value a
|
||||
reproducible decoded-tuple fingerprint, not a canonical semantic CIL identity:
|
||||
it does not resolve tokens across MVIDs, does not commit full control flow or
|
||||
exception semantics, and does not replace the raw CIL digest. Cross-build
|
||||
comparison uses the value only as one unique-only structural tier and reports
|
||||
ambiguity when that evidence is insufficient. A future normalized-CIL v2 must
|
||||
have an explicit algorithm version, canonical token/literal identities,
|
||||
ambiguity when that evidence is insufficient. A future normalized-CIL
|
||||
commitment would need canonical token/literal identities,
|
||||
complete branch and switch targets, and explicit locals/exception semantics.
|
||||
|
||||
For example, a decoded `nop; ret` body serializes exactly as
|
||||
`[["nop","none",null],["ret","none",null]]` and has v1 digest
|
||||
`[["nop","none",null],["ret","none",null]]` and has digest
|
||||
`5e5fad7741cb44bca3a4f045546b7449990da343f612f5f34c0ca30e9eee0636`.
|
||||
This vector specifies the tuple order, `null` handling, absence of whitespace,
|
||||
UTF-8 encoding, and lowercase hexadecimal output.
|
||||
@@ -170,7 +169,7 @@ that implement them. The underlying capability slices are:
|
||||
1. **Triage and inventory**: container/component classification, assembly and
|
||||
module identity, references, files, exported types, resources, attributes,
|
||||
and runtime markers.
|
||||
2. **Member inventory**: bounded types, methods, fields, properties, events,
|
||||
2. **Member inventory**: file-backed types, methods, fields, properties, events,
|
||||
interfaces, nesting, generics, and normalized signatures.
|
||||
3. **Method inspection**: headers, max stack, locals, CIL instructions,
|
||||
constants, metadata operands, exception regions, and implementation flags.
|
||||
@@ -191,9 +190,11 @@ that implement them. The underlying capability slices are:
|
||||
Evidence-backed application graph vocabulary as JavaScript/Electron
|
||||
findings while preserving managed static-analysis authority.
|
||||
|
||||
Every list has a deterministic order and hard limits. Search exposes scanned,
|
||||
matched, returned, and dropped counts. An unresolved indirect call or a failed
|
||||
signature decode is not silently omitted from a completeness claim.
|
||||
Inventory lists preserve deterministic order and include every fact available
|
||||
from the admitted PE/CLI streams. Malformed ranges and unsupported metadata are
|
||||
reported as coverage issues instead of being silently omitted. Search exposes
|
||||
scanned, matched, returned, and dropped counts. An unresolved indirect call or
|
||||
a failed signature decode is not silently omitted from a completeness claim.
|
||||
|
||||
## Obfuscation-resistant method slices
|
||||
|
||||
@@ -205,7 +206,7 @@ bounded set of observations needed to investigate a proposition:
|
||||
- exact strings, numeric constants, enum-like values, and serialization keys;
|
||||
- branch, switch, exception, loop, call, and field-flow shape;
|
||||
- construction and generic-instantiation sites;
|
||||
- raw CIL and decoded-tuple-v1 commitments, plus separate header, locals, and
|
||||
- raw CIL and decoded-tuple commitments, plus separate header, locals, and
|
||||
exception observations;
|
||||
- callers, callees, shared fields, and competing candidates; and
|
||||
- limitations from reflection, dynamic dispatch, generated code, protection,
|
||||
@@ -218,12 +219,11 @@ turn that role into the method's durable identity.
|
||||
Cross-version matching is two-stage:
|
||||
|
||||
1. Exact identity requires compatible artifact/module commitments and exact
|
||||
raw CIL identity; v1 does not expose a standalone complete-method-body
|
||||
digest.
|
||||
2. Structural identity compares schema-versioned signatures, the limited v1
|
||||
raw CIL identity; no standalone complete-method-body digest is exposed.
|
||||
2. Structural identity compares normalized signatures, the limited
|
||||
decoded-CIL tuple fingerprint, constants, and bounded shape context. It
|
||||
reports all candidates at the winning score and remains ambiguous when the
|
||||
evidence does not distinguish them. The v1 digest does not itself remap
|
||||
evidence does not distinguish them. The digest does not itself remap
|
||||
metadata tokens.
|
||||
|
||||
Tokens are always remapped through observed structure. A caller cannot carry
|
||||
@@ -327,7 +327,6 @@ absolute or relative to the manifest file. A compact manifest contains:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"label": "operator-local osu!stable semantic slice",
|
||||
"target": {
|
||||
"path": "./osu!.exe",
|
||||
@@ -373,10 +372,10 @@ Before evaluating methods, the verifier fails closed on target SHA-256, MVID,
|
||||
assembly name, runtime-family, and managed-architecture mismatches whenever the
|
||||
manifest supplies those fields. It then pages each declared MethodDef token
|
||||
directly by row, so selected methods do not need to appear in the first member
|
||||
page of a large application. `il_length` remains accepted as a legacy alias for
|
||||
`il_size`; optional `il_sha256` locks the exact raw CIL bytes in addition to
|
||||
REA's member-result-v1 decoded-instruction-tuple digest. That normalized field
|
||||
has the v1 limits documented above and is not a complete semantic CIL identity.
|
||||
page of a large application. Optional `il_sha256` locks the exact raw CIL bytes
|
||||
in addition to REA's decoded-instruction-tuple digest. That normalized field
|
||||
has the byte boundaries documented above and is not a complete semantic CIL
|
||||
identity.
|
||||
|
||||
The optional `application_graph` block reuses those exact-build method
|
||||
commitments. For each referenced MethodDef token, the verifier builds a bounded
|
||||
@@ -424,8 +423,8 @@ The managed-code track advances as reviewable pull requests:
|
||||
|
||||
1. accepted evidence/provider boundary (this document and ADR);
|
||||
2. read-only artifact triage and exact identity (shipped);
|
||||
3. bounded metadata, signatures, method bodies, raw CIL hashes, and the limited
|
||||
decoded-instruction-tuple fingerprint v1 (shipped; normalized-CIL v2
|
||||
3. file-backed metadata, signatures, method bodies, raw CIL hashes, and the
|
||||
decoded-instruction-tuple fingerprint (shipped; complete normalized-CIL
|
||||
semantics remain planned);
|
||||
4. obfuscation-resistant slices and cross-version comparison (shipped for
|
||||
static member observations);
|
||||
|
||||
+33
-56
@@ -2,14 +2,11 @@
|
||||
|
||||
## Identity and discovery
|
||||
|
||||
`rea://server/identity` and `binary_session` report the package, server, SDK,
|
||||
negotiated protocol, skill, and schema-sensitive catalog identities separately.
|
||||
An absent live comparison is `unknown`, never aligned. Callers can supply the
|
||||
expected package version, catalog digest, or registered server path to
|
||||
`binary_session`; a mismatch means the registered MCP process must be restarted.
|
||||
`rea doctor` separately inspects supported JSON/TOML client registrations,
|
||||
reports their command vectors as aligned, stale, missing, or invalid, and keeps
|
||||
live-server state `unknown` unless the active connection supplies identity.
|
||||
`binary_session` reports the active package, server, SDK, and negotiated
|
||||
protocol details. `rea doctor` separately inspects supported JSON/TOML client
|
||||
registrations, reports their command vectors as aligned, stale, missing, or
|
||||
invalid, and keeps live-server state `unknown` unless the active connection
|
||||
supplies identity.
|
||||
|
||||
Canonical tool names remain stable, while `tools/list` advertises only the
|
||||
operations callable for the current target, provider, policy, host, and
|
||||
@@ -48,7 +45,7 @@ REA accepts ordinary `tools/call` progress tokens. Updates are monotonic,
|
||||
rate-bounded to at most one intermediate update per 100 ms, and always allow a
|
||||
terminal update. Unknown totals are omitted; REA does not fabricate percentages.
|
||||
Provider calls receive the request cancellation signal. Artifact traversal,
|
||||
hashing, cross-version scanning/checkpoints, Hopper requests, and process capture
|
||||
hashing, version comparisons, Hopper requests, and process capture
|
||||
check the same signal. Cancellation is distinct from timeout. A cleanup failure
|
||||
uses `cleanup_incomplete` and lists only the owned resource kinds that remain.
|
||||
Derived comparisons and reconstruction verification yield before computation
|
||||
@@ -58,36 +55,24 @@ CLI calls work without a progress token and translate SIGINT into the same
|
||||
AbortSignal used by providers. Existing controlled-process cleanup and provider
|
||||
shutdown rules still apply; REA never kills a process it cannot prove it owns.
|
||||
|
||||
## Evidence resources
|
||||
## Tool results
|
||||
|
||||
Evidence remains inline for compatibility and successful Evidence-producing
|
||||
tools also return `resource_link` content. Session resources are connection-local:
|
||||
Evidence-producing tools return their result and Evidence context inline. Read
|
||||
that result directly. Use `get_evidence_bundle` when the task needs broader
|
||||
retained session history or an explicit bundle for transfer.
|
||||
|
||||
- `rea://evidence/{evidenceId}` returns the complete immutable Evidence v2 record.
|
||||
- `rea://evidence/{evidenceId}/section/{section}` returns a bounded result section.
|
||||
Stable sections include `result`, `terminal`, `filesystem`, `process`,
|
||||
`protocol`, `nodes`, `occurrences`, and `edges` when present.
|
||||
- `rea://unknown/{unknownId}` returns the current residual-unknown head and its
|
||||
immutable revision history.
|
||||
- `rea://unknowns/active` returns current unresolved heads.
|
||||
- `rea://snapshot/{snapshotDigest}` returns the current immutable analysis
|
||||
snapshot when its canonical content digest matches.
|
||||
- `rea://artifact/{manifestId}/{collection}` returns a canonical artifact
|
||||
`nodes`, `occurrences`, or `edges` page with Evidence provenance.
|
||||
- `rea://function/{targetSha256}/{address}` returns a retained function dossier
|
||||
for the exact target and address.
|
||||
- `rea://workspace/{workspaceId}/revision/{revision}` returns an immutable,
|
||||
CAS-linked investigation workspace revision retained by this session.
|
||||
## Aggregate native context
|
||||
|
||||
Successful automatic cross-version investigations include both Evidence and
|
||||
workspace `resource_link` blocks. Workspace resources preserve revision and
|
||||
`previous_revision_digest` commitments; persistent workspace files remain
|
||||
subject to configured read/write roots.
|
||||
|
||||
An Evidence URI is discoverability, not authorization. It cannot authorize file
|
||||
access, extraction, mounting, execution, or networking. IDs disappear when the
|
||||
session ends unless the existing Evidence bundle or workspace persistence flow
|
||||
explicitly retains them.
|
||||
`get_navigation_context` composes the selected document, current address, and
|
||||
current/containing procedure. Its capability inventory exposes a
|
||||
`current_selection` mode and an `explicit_document` mode; the latter works when
|
||||
the caller supplies `document` and the provider lacks `current_document`. A
|
||||
cursor outside a procedure is represented as
|
||||
`procedure: null`. `inspect_address_context` requires an explicit address and
|
||||
returns name, procedure, comment, inline-comment, and bookmark facets;
|
||||
unsupported facets are local `unavailable` outcomes. Use `current_document`,
|
||||
`current_address`, and `current_procedure` for direct single-field lookups; use
|
||||
the aggregate tools when you need related context together.
|
||||
|
||||
## Permission policy
|
||||
|
||||
@@ -100,14 +85,15 @@ Existing environment settings map to administrator ceilings. They remain the
|
||||
maximum authority:
|
||||
|
||||
- process roots, executables, environment names, and external networking;
|
||||
- Evidence read/write roots;
|
||||
- investigation input and workspace roots;
|
||||
- snapshot read/write roots;
|
||||
- reference-source roots;
|
||||
- native mount enablement.
|
||||
|
||||
Artifact extraction retains its existing explicit per-call approval and maps to
|
||||
an administrator root ceiling of `/` for compatibility.
|
||||
Evidence bundles and analysis snapshots use the path supplied to the CLI or MCP
|
||||
tool; they do not require configured roots or separate read/write grants.
|
||||
|
||||
Artifact extraction takes no path or occurrence selectors. It materializes all
|
||||
regular files into a fresh REA-chosen temporary directory. The write still
|
||||
passes through the active permission evaluator; the tool caller does not choose
|
||||
an output path.
|
||||
|
||||
`rea policy status`, `list`, `explain`, and `revoke` inspect the same evaluator
|
||||
used by MCP. Optional project grants require both
|
||||
@@ -137,11 +123,7 @@ administrator ceiling. A request outside that ceiling reports
|
||||
`elicitation_supported: false` and `restart_required: true`; interactive consent
|
||||
cannot silently widen administrator policy.
|
||||
|
||||
`analyze_javascript_application` uses the `investigation_input` capability. Its
|
||||
absolute `input_path` must be inside `REA_INVESTIGATION_INPUT_ROOTS_JSON` and the
|
||||
request must set `approved: true` before any artifact read. Reading source-map
|
||||
contents additionally requires `source_map_read_approved: true`; ordinary input
|
||||
approval does not imply that separate authority.
|
||||
`analyze_javascript_application` reads the supplied local directory or ASAR path directly and returns its result and Evidence inline; no approval flag or root configuration is needed.
|
||||
|
||||
## Integrity record-and-continue
|
||||
|
||||
@@ -152,12 +134,7 @@ all three conditions:
|
||||
2. `integrity_policy=record-and-continue`;
|
||||
3. explicit per-call `integrity_continue_approved=true`.
|
||||
|
||||
Contradictory bytes are quarantined from nested expansion, recorded with declared
|
||||
and observed hashes, trust, provenance, path, and unpacked state, and bounded by
|
||||
`max_integrity_mismatches`. Verified siblings continue. Comparisons classify the
|
||||
result as a contradiction and reconstruction cannot treat it as unchanged.
|
||||
`investigate-versions` accepts the same policy through
|
||||
`--integrity-policy record-and-continue --integrity-continue-approved` and
|
||||
`--max-integrity-mismatches`. Its Evidence pages and CAS-linked workspace retain
|
||||
contradictions, so a completed run can resume without rescanning or weakening
|
||||
trust labels.
|
||||
Contradictory bytes are quarantined from nested expansion and recorded with
|
||||
declared and observed hashes, trust, provenance, path, and unpacked state.
|
||||
Verified siblings continue. Comparisons classify the result as a contradiction
|
||||
and reconstruction cannot treat it as unchanged.
|
||||
|
||||
+39
-37
@@ -6,19 +6,28 @@ invoke tools by themselves.
|
||||
|
||||
## Prompt inventory
|
||||
|
||||
| Prompt | Purpose |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `investigate_feature` | Trace a feature from discovery into bounded function evidence. |
|
||||
| `compare_application_versions` | Compare two shipped artifacts, with optional static and runtime follow-up. |
|
||||
| `verify_reconstruction` | Evaluate a finite reconstruction specification against retained comparison Evidence. |
|
||||
| `trace_crash` | Correlate a crash symptom with static paths and optional Process Capture v4 evidence. |
|
||||
| `audit_residual_unknowns` | Audit current residual-unknown heads and evidence-qualified resolution. |
|
||||
| `prepare_bounded_process_capture` | Design an approval-gated, bounded process experiment before execution. |
|
||||
| Prompt | Purpose |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `investigate_feature` | Trace a feature from discovery into relevant function evidence. |
|
||||
| `compare_application_versions` | Compare two shipped artifacts, with optional static and runtime follow-up. |
|
||||
| `verify_reconstruction` | Evaluate a finite reconstruction specification against retained comparison Evidence. |
|
||||
| `trace_crash` | Correlate a crash symptom with static paths and optional process capture evidence. |
|
||||
| `audit_residual_unknowns` | Audit current residual-unknown heads and evidence-qualified resolution. |
|
||||
| `prepare_bounded_process_capture` | Design an approval-gated controlled process experiment before execution. |
|
||||
|
||||
Every rendered prompt provides an ordered list of current REA tool names. It
|
||||
also requires the agent to keep observations, inferences, and unknowns
|
||||
distinct. Requested prompt arguments and completion choices are rendered as
|
||||
untrusted selection data, not instructions or authorization.
|
||||
Every rendered prompt provides optional starting points using current REA tool
|
||||
names. Agents can call tools directly, skip irrelevant suggestions, and inspect
|
||||
provider or target state only when it helps answer the request or a tool
|
||||
requires it. The prompts also ask agents to keep observations, inferences, and
|
||||
unknowns distinct. Requested prompt arguments and completion choices are
|
||||
rendered as untrusted selection data, not instructions or authorization.
|
||||
Tool results include their Evidence inline. Prompts do not require a bundle or
|
||||
resource fetch to read a result; an Evidence ID is only a reference when a
|
||||
prompt argument or tool explicitly accepts one.
|
||||
|
||||
For target paths in `compare_application_versions`, the prompt suggests
|
||||
opening each target before calling `inspect_artifact`; inspection operates on
|
||||
the active target and returns the graph and findings together inline.
|
||||
|
||||
Use standard MCP discovery and retrieval:
|
||||
|
||||
@@ -45,11 +54,10 @@ cached prompt catalog.
|
||||
|
||||
## Session-aware completion
|
||||
|
||||
MCP `completion/complete` currently completes prompt arguments and resource
|
||||
template variables. It does not define completion for arbitrary tool-call
|
||||
arguments. REA therefore attaches completion to optional guided-prompt
|
||||
arguments that feed later tool selection; it does not claim protocol-level
|
||||
completion for every REA tool.
|
||||
MCP `completion/complete` completes guided-prompt arguments; it does not define
|
||||
completion for arbitrary tool-call arguments. REA attaches completion to
|
||||
optional prompt arguments that accept a session value. Suggestions provide
|
||||
identifiers only; they do not retrieve or replace the inline Evidence result.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -74,28 +82,22 @@ completion for every REA tool.
|
||||
|
||||
Completion sources are live projections of the current session:
|
||||
|
||||
| Argument family | Source and filtering |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Document | Current provider `list_documents` result. |
|
||||
| Procedure | Bounded pagination over `list_procedures`; observed addresses are offered from partial results, while names require exhaustive discovery and a unique address. Optional document context is forwarded. |
|
||||
| Provider | `auto` plus sorted deep candidates from the current binary session; target-free completion does not start them. |
|
||||
| Evidence | Evidence IDs retained in the current session ledger. |
|
||||
| Process capture | Evidence IDs whose operation and validated result identify Process Capture v4. |
|
||||
| Artifact manifest | Manifest IDs from schema-valid retained artifact inventories. |
|
||||
| Artifact occurrence | Occurrence IDs present in schema-valid retained artifact inventory pages. |
|
||||
| Residual unknown | Current non-resolved unknown heads only. |
|
||||
| Argument family | Source and filtering |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Document | Current provider `list_documents` result. |
|
||||
| Procedure | The complete `list_procedures` inventory; all procedure addresses and uniquely named procedures are offered. Optional document context is forwarded. |
|
||||
| Provider | `auto` plus sorted deep candidates from the current binary session; target-free completion does not start them. |
|
||||
| Evidence | Evidence IDs retained in the current session ledger, offered only for prompt arguments that accept Evidence references. |
|
||||
| Process capture | IDs for retained Evidence whose operation and validated result identify process capture. |
|
||||
| Artifact manifest | Manifest IDs from schema-valid retained artifact inventories. |
|
||||
| Artifact occurrence | Occurrence IDs present in schema-valid retained artifact inventory pages. |
|
||||
| Residual unknown | Current non-resolved unknown heads only. |
|
||||
|
||||
Suggestions are Unicode-normalized, case-insensitive prefix matches. The server
|
||||
deduplicates them, rejects values longer than 4,096 characters, and sorts by
|
||||
code point for deterministic results. It scans at most 10,000 candidates;
|
||||
procedure discovery is further bounded to 5,000 unique addresses in pages of 500. If that bound is reached before exhaustive procedure discovery, only
|
||||
observed addresses are suggested because global name uniqueness is unknown.
|
||||
|
||||
The MCP result carries at most 100 `values`. `total` is the number of matching
|
||||
candidates found within REA's scan bound, and `hasMore` reports whether more
|
||||
than 100 were found. The MCP completion response has no offset or cursor, so a
|
||||
client narrows a large result by sending a longer prefix rather than requesting
|
||||
another page.
|
||||
deduplicates them and sorts by code point for deterministic results. Procedure
|
||||
completion derives candidates from the complete inventory, so a name is offered
|
||||
only when it identifies one address in that inventory. Completion returns all
|
||||
matching values; it does not expose paging controls.
|
||||
|
||||
## Lifecycle and safety
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
# Native UI and dispatch investigation
|
||||
|
||||
REA joins static resources, native metadata and code facts while keeping runtime
|
||||
observations separate. Every result reports its evidence, target identity,
|
||||
coverage and unknowns. The CLI and MCP use the same application workflows.
|
||||
|
||||
## Static inspection
|
||||
|
||||
- `inspect_asset_catalog` / `rea inspect-asset-catalog <app>` reads compiled
|
||||
`Assets.car` metadata through macOS `assetutil --info`. Catalog digests, raw
|
||||
rendition fields, pagination and exact UI resource-name matches are returned.
|
||||
Image extraction and undocumented rendition interpretation are unsupported.
|
||||
- `inspect_keyed_archive` / `rea inspect-keyed-archive <plist>` reads XML or
|
||||
binary Foundation keyed archives without instantiating classes. For an active
|
||||
app, provide its relative archive path as the second CLI argument or MCP
|
||||
`path`. Results retain original `$objects` indices, named `$top` roots,
|
||||
shared/cyclic references, class descriptors, raw serialized fields and
|
||||
malformed/unresolved/nil references. Missing fields stay absent; unknown
|
||||
classes are preserved. Select `root`, `offset` and `limit` explicitly.
|
||||
UID 0 cannot distinguish conditional nil from ordinary nil. Limits are
|
||||
64 MiB of input, 200,000 objects/references and 128 nesting levels.
|
||||
- `decode_interface_builder` / `rea decode-interface-builder <app>` reads
|
||||
compiled storyboard and nib resources into objects, outlets, actions,
|
||||
connections and controller/class names. `verify:interface-builder` compiles
|
||||
a source-owned AppKit XIB with `ibtool`. Storyboards require an installed
|
||||
iOS platform; unsupported archive forms remain explicit.
|
||||
- `inspect_native_dispatch_metadata` / `rea inspect-native-dispatch-metadata
|
||||
<app-or-binary>` prefers a validated macOS Mach-O byte reader. It decodes
|
||||
64-bit little-endian Objective-C class/metaclass records, superclass pointers,
|
||||
absolute/relative method entries, ivar offsets/sizes/alignment and protocol
|
||||
declarations. It also decodes simple Swift conformances, static synchronous
|
||||
witness slots, signed-relative pointers and non-generic, non-resilient class
|
||||
vtable descriptors. Records include exact virtual addresses/file offsets and
|
||||
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
|
||||
start after the conformance header. Names unavailable in metadata remain null.
|
||||
Chained fixups, external bindings, authenticated pointers, categories,
|
||||
properties, generic/resilient/async/coroutine tables and inherited overrides
|
||||
have explicit unsupported or partial coverage. Other providers retain the
|
||||
existing symbol-based inventory with its narrower coverage.
|
||||
|
||||
## Native instruction, call and type primitives
|
||||
|
||||
Ghidra supplies three exact-object operations:
|
||||
|
||||
```bash
|
||||
rea inspect-native-instruction <binary> <address> --provider ghidra
|
||||
rea resolve-native-call-targets <binary> <call-site> --provider ghidra
|
||||
rea inspect-native-data-type <binary> --type /MyStruct --provider ghidra
|
||||
rea inspect-native-data-type <binary> --address <typed-data-address> --provider ghidra
|
||||
```
|
||||
|
||||
Instruction facts include bytes, length, decoder text, ordered register/scalar/
|
||||
address tokens, typed references and flow destinations. Mid-instruction, data,
|
||||
outside-memory and undecodable addresses have separate outcomes. Effective
|
||||
memory base/index/displacement roles and per-instruction context mode remain
|
||||
unavailable; `mode` is the program language variant.
|
||||
|
||||
Call resolution reports direct, resolved indirect, ambiguous, unresolved and
|
||||
non-call outcomes from static call references. It does not establish runtime
|
||||
execution or classify Objective-C/Swift/vtable/closure mechanisms from names.
|
||||
|
||||
Type inspection selects one exact database pathname or typed data address.
|
||||
Struct/union fields, enums, pointers, arrays, typedefs, size, alignment, packing
|
||||
and bitfields retain database authority. Child types use exact IDs for further
|
||||
inspection, including recursive layouts. Source/debug authority and flexible
|
||||
array semantics are not inferred. The real lane uses a native DWARF 4 object
|
||||
with a struct containing a union, enum and recursive pointer; a stripped linked
|
||||
binary may have no corresponding recovered layout.
|
||||
|
||||
## Bounded static traces
|
||||
|
||||
`trace_native_ui_action` / `rea trace-native-ui-action <target> <seed>` accepts
|
||||
one authored selector/object ID, native symbol or exact function address. It
|
||||
joins UI wiring to uniquely matched class/selector implementations and follows
|
||||
bounded typed call references. Direct references, resolved indirect references,
|
||||
ambiguous candidates, inferred untyped provider callees and targetless call
|
||||
sites remain distinct. A static route does not establish runtime reachability.
|
||||
Swift/closure dispatch requires resolved static references; unavailable ABI
|
||||
forms remain unknown.
|
||||
|
||||
`trace_native_values` / `rea trace-native-values <binary> <procedure> --provider
|
||||
ghidra` composes high-p-code def-use graphs across statically resolved calls.
|
||||
Logical CALL arguments bind to recovered parameter ordinals; recovered RETURN
|
||||
values bind to CALL outputs. These edges are decompiler-derived. Constants,
|
||||
operators, LOAD/STORE and branch operands are included inline. Alias effects,
|
||||
persistent state, RNG roles, missing/variadic bindings and ambiguous destinations
|
||||
remain unknown. Budgets control depth, decompilations, call-site resolutions,
|
||||
nodes and edges. Node payloads are bounded to 8 MiB and serialized results to
|
||||
32 MiB. Pagination returns edges whose source is on the node page and preserves
|
||||
stable IDs for endpoints outside that page.
|
||||
|
||||
The raw Java `is_dead` flag can remain set after Ghidra decodes a live block;
|
||||
`block_membership` reports actual syntax-tree membership separately.
|
||||
|
||||
The provider dossier itself retains up to 3,000 p-code operations, 64 inputs per
|
||||
operation and 12,000 def-use edges, with explicit omitted counts. A `STORE`
|
||||
may describe stack memory and does not itself prove persistent state.
|
||||
|
||||
AArch64 jump-table recovery additionally verifies byte and halfword relative
|
||||
forms from unsigned bounds, register definitions, table loads, branch bases,
|
||||
scaled ADD/BR instructions and the recovered target set. It reads exactly the
|
||||
proven count and preserves unknowns for other forms. Real ELF and host ARM64
|
||||
Mach-O fixtures check each case against source-owned return values.
|
||||
|
||||
## Approved native desktop observation
|
||||
|
||||
`observe_native_ui` captures one explicit existing PID/window ID after
|
||||
`observation_approved: true`. Screenshots use a selected-window ScreenCaptureKit
|
||||
filter on macOS 14+; accessibility reads stay within that window. Missing Screen
|
||||
Recording/Accessibility permissions produce actionable errors without broad
|
||||
capture or automatic permission prompts. Executable bytes and process launch
|
||||
time guard against a different target or PID reuse. AX selection requires one
|
||||
unique geometry match; ambiguity fails closed.
|
||||
|
||||
`capture_native_ui_scenario` additionally requires `actions_approved: true` and
|
||||
`restore: "leave-as-is"`. Steps select AX child-index paths for press, increment/
|
||||
decrement scrolling and text-value entry, or bounded waits. No global event
|
||||
injection is used. Unsupported AX actions fail explicitly. The result preserves
|
||||
ordered before/after captures and gaps; an action may have occurred before a
|
||||
post-action capture fails. The caller must choose whether to recover app state.
|
||||
|
||||
Scenarios allow 16 steps, 30 seconds of total waits, 2,000 AX nodes per capture,
|
||||
8 MiB PNGs and 64 MiB output within a 180-second deadline. REA compiles one owned
|
||||
helper per scenario, removes its temporary compiler cache and stops its helper
|
||||
on cancellation. It does not launch or own the selected application. Approved
|
||||
UI actions may change application data or trigger network activity.
|
||||
|
||||
## Provider and verification boundaries
|
||||
|
||||
Ghidra 12.1.4 with a full 64-bit JDK 21 is bring-your-own. Linux x64 and macOS
|
||||
x64/arm64 are admitted; macOS requires the matching native decompiler. Windows
|
||||
x64 P0 remains limited to approved native PE applications. There is no GUI or
|
||||
mutation authority and no automatic fallback to Hopper.
|
||||
|
||||
- `npm run verify:ghidra`: host-native debug/stripped targets, native type layout,
|
||||
instruction/call facts, value dependencies and process/project cleanup.
|
||||
- `npm run verify:ghidra:aarch64-jump-table`: optimized ELF and byte/halfword
|
||||
relative tables, plus ARM64 Mach-O on an ARM64 macOS host.
|
||||
- `npm run verify:apple-dispatch`: source-built Objective-C protocols/classes and
|
||||
Swift conformances/vtables, repeated after stripping local symbols.
|
||||
- `npm run verify:native-ui`: one source-owned fixture window, selected capture
|
||||
or a reported OS permission denial, changed-target rejection and cleanup.
|
||||
|
||||
macOS ARM64 is the real host verified during this implementation. Admission of
|
||||
macOS Intel does not claim an Intel verification run. Unsupported metadata and
|
||||
unresolved runtime/value semantics remain visible in results.
|
||||
+38
-23
@@ -1,4 +1,4 @@
|
||||
# Process Capture v4
|
||||
# Process capture
|
||||
|
||||
Process Capture records one approved command as deterministic Evidence. It is
|
||||
intended for authority-versus-reconstruction checks where terminal behavior,
|
||||
@@ -11,7 +11,7 @@ HTTP/WebSocket exchanges, and process exit ownership. Missing or bounded
|
||||
observations remain explicit; a truncated capture is never treated as
|
||||
equivalent to another capture.
|
||||
|
||||
Every v4 capture includes a run manifest with canonical SHA-256 commitments for
|
||||
Every capture includes a run manifest with canonical SHA-256 commitments for
|
||||
the secret-safe full scenario projection, comparison contract, executable,
|
||||
normalization rules, command-shim plan, and replay plan. The manifest also
|
||||
records the REA/provider versions, platform, architecture, PTY backend, and UTC
|
||||
@@ -47,18 +47,19 @@ REA offers the once-or-session grant form only after the connection negotiates
|
||||
an MCP revision with multi-round tool results and advertises form elicitation.
|
||||
Older connections keep returning the structured
|
||||
permission-required result and never wait for an unsupported elicitation
|
||||
response. The MCP SDK runtime currently pinned by REA negotiates the older
|
||||
protocol path, so the interactive flow remains fail-closed until its runtime
|
||||
protocol support is available.
|
||||
response. On the pinned `@modelcontextprotocol/server` 2.0.0 runtime, this is
|
||||
the official `2026-07-28` `inputRequired` continuation: the SDK seals the
|
||||
request state and validates the continuation envelope, while REA validates the
|
||||
untrusted input response before creating a connection-owned grant. The older
|
||||
push-style `elicitation/create` path is not used for this protocol revision.
|
||||
|
||||
Every scenario must also contain `"approved": true`. Executables, working
|
||||
directories, filesystem roots, and requested environment variables are checked
|
||||
against operator policy before launch. Process Capture is an observation tool,
|
||||
Executables, working directories, filesystem roots, and requested environment
|
||||
variables are checked against operator policy before launch. Process Capture is an observation tool,
|
||||
not a security sandbox: the target runs with the current user's permissions.
|
||||
|
||||
## Capture a scenario
|
||||
|
||||
`rea capture-process` reads a JSON scenario and writes Process Capture v4
|
||||
`rea capture-process` reads a JSON scenario and writes process capture
|
||||
Evidence to stdout:
|
||||
|
||||
```bash
|
||||
@@ -69,7 +70,6 @@ A representative scenario is:
|
||||
|
||||
```json
|
||||
{
|
||||
"approved": true,
|
||||
"executable": "/absolute/path/to/node",
|
||||
"arguments": ["./signup.mjs"],
|
||||
"working_directory": "/absolute/project",
|
||||
@@ -115,13 +115,20 @@ redacted from recorded text. The command-shim directory is placed first in the
|
||||
captured process `PATH`; any remaining `PATH` must be explicitly supplied or
|
||||
inherited under operator policy.
|
||||
|
||||
Scenario arguments, environment names, timed interactions, shim routes and
|
||||
output chunks, filesystem checkpoints, and static HTTP/WebSocket scripts are
|
||||
accepted without fixed item-count ceilings. The run still obeys its timeout
|
||||
and the configured output-byte, process, filesystem, protocol-event, body-byte,
|
||||
and connection budgets; each filesystem checkpoint uses those same
|
||||
bounded snapshot limits.
|
||||
|
||||
## Reactive process scenarios
|
||||
|
||||
Set `reactive` to a Process Reactive Scenario v1 declaration when interaction
|
||||
Set `reactive` to a process reactive scenario declaration when interaction
|
||||
must follow observed output instead of guessed delays. The graph declares an
|
||||
initial state, absolute scenario and state deadlines, explicitly prioritized
|
||||
transitions, bounded uses and visits, trigger predicates, ordered actions, and
|
||||
either a next state or `passed` outcome.
|
||||
transitions, caller-chosen use and visit counts, trigger predicates, ordered
|
||||
actions, and either a next state or `passed` outcome.
|
||||
|
||||
The admitted runtime slice matches decoded UTF-8 terminal literals and exact
|
||||
`terminal_raw`, `interaction`, `process`, `filesystem`, `http`, `websocket`, or
|
||||
@@ -129,7 +136,7 @@ The admitted runtime slice matches decoded UTF-8 terminal literals and exact
|
||||
event-complete lifecycle. Filesystem events identify named checkpoints; shim
|
||||
events identify a command and route. Live predicates receive the same normalized
|
||||
and redacted payload later persisted in Evidence. Predicates may compose with
|
||||
bounded `all`, `any`, `sequence`, and `repeat` nodes and may select a frontier at
|
||||
`all`, `any`, `sequence`, and `repeat` nodes and may select a frontier at
|
||||
scenario start, state entry, a prior checkpoint, or an exact event ID. Actions
|
||||
can write PTY input, resize it, signal the root process, or capture a named
|
||||
filesystem checkpoint. `terminal_rendered`, `lifecycle`, and `replay_transition`
|
||||
@@ -138,9 +145,12 @@ target launches.
|
||||
|
||||
Reactive results are recorded in `reactive_run`, including the terminal outcome,
|
||||
active state, matched event IDs, action evidence IDs, and deterministic
|
||||
transition journal. Terminal controls also commit the last admitted observation
|
||||
order, so later output cannot be moved ahead of a timeout, cancellation, or
|
||||
target loss during validation. If the process exits before the graph finishes,
|
||||
transition journal. The reducer retains and evaluates every admitted
|
||||
observation through the scenario deadline; it has no fixed state, transition,
|
||||
predicate, action, repeat, or history-count ceilings. Recursive input depth has
|
||||
a stack safety guard. Terminal controls also commit the last admitted
|
||||
observation order, so later output cannot be moved ahead of a timeout,
|
||||
cancellation, or target loss during validation. If the process exits before the graph finishes,
|
||||
the outcome is `target_lost`; state/scenario deadlines remain distinct
|
||||
`predicate_timeout`/`scenario_deadline` outcomes. Timed `events` remain
|
||||
available for fixed schedules, but they are not silently translated into the
|
||||
@@ -156,9 +166,10 @@ the initial tree.
|
||||
|
||||
Each command shim matches an exact argument array. Routes can emit timed stdout
|
||||
or stderr chunks and then exit or receive `SIGINT`, `SIGTERM`, or `SIGKILL`.
|
||||
Unmatched and exhausted calls are recorded. Shim observations are bounded by
|
||||
the scenario protocol-event limit; exceeding it marks the whole capture
|
||||
truncated while replay continues.
|
||||
Unmatched and exhausted calls are recorded. Shim observations are included in
|
||||
the capture without a separate fixed count ceiling. Other configured
|
||||
observation and byte budgets remain explicit in the capture result when they
|
||||
affect completeness.
|
||||
|
||||
## Run a replay machine directly
|
||||
|
||||
@@ -171,9 +182,13 @@ transition. It never returns request bodies, request headers, or captured
|
||||
values. It also reports the initial and final states, whether the final state is
|
||||
terminal, configured limits, and committed usage.
|
||||
|
||||
The direct boundary accepts at most 10,000 events, 4 MiB of declared action
|
||||
JSON, 1,024 action text fields, and 1,024 possible sensitive-capture operations.
|
||||
These caps bound both returned content and secret-redaction work.
|
||||
The direct evaluator has no fixed state, transition, event, guard, capture,
|
||||
action, or action-byte count ceiling. It indexes transitions by protocol and
|
||||
path, evaluates events in order, and returns a decision for each offered event
|
||||
plus actions for used transitions. Caller-declared `max_uses`, `max_visits`,
|
||||
`max_transitions`, and protocol budgets determine the finite run; there is no
|
||||
upper schema cap on those values. Result size follows the input and is not
|
||||
truncated by an arbitrary item quota.
|
||||
|
||||
Refused events remain data rather than aborting the run. Outcomes distinguish
|
||||
unmatched events, invalid states, failed guards, exhausted transitions, invalid
|
||||
|
||||
+73
-202
@@ -1,22 +1,21 @@
|
||||
{
|
||||
"catalog_schema_version": 1,
|
||||
"package": {
|
||||
"name": "rea-agents",
|
||||
"version": "2.5.1",
|
||||
"version": "3.2.1",
|
||||
"sdk": {
|
||||
"server": "2.0.0-beta.4",
|
||||
"client": "2.0.0-beta.4",
|
||||
"core": "2.0.0-beta.4"
|
||||
"server": "2.0.0",
|
||||
"client": "2.0.0",
|
||||
"core": "2.0.0"
|
||||
},
|
||||
"skill_version": "23"
|
||||
},
|
||||
"tools": {
|
||||
"total": 110,
|
||||
"total": 122,
|
||||
"families": [
|
||||
{
|
||||
"id": "direct",
|
||||
"surface": "official-proxy",
|
||||
"count": 36,
|
||||
"count": 39,
|
||||
"tools": [
|
||||
"address_name",
|
||||
"address_to_file_offset",
|
||||
@@ -26,6 +25,8 @@
|
||||
"current_procedure",
|
||||
"goto_address",
|
||||
"inline_comment",
|
||||
"inspect_native_data_type",
|
||||
"inspect_native_instruction",
|
||||
"list_bookmarks",
|
||||
"list_documents",
|
||||
"list_names",
|
||||
@@ -44,6 +45,7 @@
|
||||
"read_bytes",
|
||||
"read_function_instructions",
|
||||
"resolve_containing_procedure",
|
||||
"resolve_native_call_targets",
|
||||
"search_procedures",
|
||||
"search_strings",
|
||||
"set_address_name",
|
||||
@@ -59,7 +61,7 @@
|
||||
{
|
||||
"id": "enhanced",
|
||||
"surface": "enhanced",
|
||||
"count": 12,
|
||||
"count": 15,
|
||||
"tools": [
|
||||
"analyze_function",
|
||||
"analyze_swift_types",
|
||||
@@ -70,28 +72,39 @@
|
||||
"get_call_graph",
|
||||
"get_objc_classes",
|
||||
"get_objc_protocols",
|
||||
"swift_classes",
|
||||
"inspect_native_api",
|
||||
"inspect_native_dispatch_metadata",
|
||||
"trace_call_path",
|
||||
"trace_feature"
|
||||
"trace_feature",
|
||||
"trace_native_ui_action",
|
||||
"trace_native_values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "native",
|
||||
"surface": "native-provider",
|
||||
"count": 5,
|
||||
"count": 7,
|
||||
"tools": [
|
||||
"capture_native_ui_scenario",
|
||||
"demangle_swift",
|
||||
"inspect_macho",
|
||||
"inspect_plist",
|
||||
"inspect_signature",
|
||||
"list_architectures"
|
||||
"list_architectures",
|
||||
"observe_native_ui"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact",
|
||||
"surface": "artifact-provider",
|
||||
"count": 3,
|
||||
"tools": ["extract_artifact", "inspect_artifact", "inventory_artifact"]
|
||||
"count": 5,
|
||||
"tools": [
|
||||
"decode_interface_builder",
|
||||
"extract_artifact",
|
||||
"inspect_artifact",
|
||||
"inspect_asset_catalog",
|
||||
"inspect_keyed_archive"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "managed",
|
||||
@@ -127,9 +140,10 @@
|
||||
{
|
||||
"id": "electron",
|
||||
"surface": "electron-provider",
|
||||
"count": 4,
|
||||
"count": 5,
|
||||
"tools": [
|
||||
"analyze_javascript_application",
|
||||
"capture_electron_scenario",
|
||||
"inspect_electron_page",
|
||||
"list_electron_targets",
|
||||
"reconcile_javascript_runtime"
|
||||
@@ -147,17 +161,15 @@
|
||||
{
|
||||
"id": "application",
|
||||
"surface": "application-workflow",
|
||||
"count": 12,
|
||||
"count": 10,
|
||||
"tools": [
|
||||
"build_reconstruction_obligation_ledger",
|
||||
"commit_reconstruction_coverage",
|
||||
"compare_application_versions",
|
||||
"compare_javascript_export_shapes",
|
||||
"compare_source_to_bundle",
|
||||
"evaluate_reconstruction_readiness",
|
||||
"evaluate_reconstruction_coverage",
|
||||
"execute_node_characterization",
|
||||
"prepare_node_characterization",
|
||||
"query_reconstruction_coverage",
|
||||
"run_controlled_replay",
|
||||
"trace_application_feature",
|
||||
"trace_javascript_semantics"
|
||||
@@ -166,7 +178,7 @@
|
||||
{
|
||||
"id": "session",
|
||||
"surface": "session",
|
||||
"count": 19,
|
||||
"count": 22,
|
||||
"tools": [
|
||||
"binary_session",
|
||||
"build_call_path",
|
||||
@@ -179,7 +191,10 @@
|
||||
"correlate_static_and_runtime",
|
||||
"export_evidence_bundle",
|
||||
"find_changed_behavior",
|
||||
"get_evidence_bundle",
|
||||
"get_navigation_context",
|
||||
"import_evidence_bundle",
|
||||
"inspect_address_context",
|
||||
"list_unknowns",
|
||||
"open_binary",
|
||||
"record_unknown",
|
||||
@@ -199,6 +214,8 @@
|
||||
"capabilities": [
|
||||
"address_name",
|
||||
"analyze_function",
|
||||
"inspect_native_data_type",
|
||||
"inspect_native_instruction",
|
||||
"list_documents",
|
||||
"list_names",
|
||||
"list_procedures",
|
||||
@@ -213,6 +230,7 @@
|
||||
"procedure_references",
|
||||
"read_function_instructions",
|
||||
"resolve_containing_procedure",
|
||||
"resolve_native_call_targets",
|
||||
"search_procedures",
|
||||
"search_strings",
|
||||
"xrefs"
|
||||
@@ -267,11 +285,13 @@
|
||||
"name": "macOS native inspection utilities",
|
||||
"version": null,
|
||||
"capabilities": [
|
||||
"capture_native_ui_scenario",
|
||||
"demangle_swift",
|
||||
"inspect_macho",
|
||||
"inspect_plist",
|
||||
"inspect_signature",
|
||||
"list_architectures"
|
||||
"list_architectures",
|
||||
"observe_native_ui"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -279,9 +299,11 @@
|
||||
"name": "REA safe artifact graph provider",
|
||||
"version": "1",
|
||||
"capabilities": [
|
||||
"decode_interface_builder",
|
||||
"extract_artifact",
|
||||
"inspect_artifact",
|
||||
"inventory_artifact"
|
||||
"inspect_asset_catalog",
|
||||
"inspect_keyed_archive"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -339,14 +361,12 @@
|
||||
"version": "1",
|
||||
"capabilities": [
|
||||
"build_reconstruction_obligation_ledger",
|
||||
"commit_reconstruction_coverage",
|
||||
"compare_application_versions",
|
||||
"compare_javascript_export_shapes",
|
||||
"compare_source_to_bundle",
|
||||
"evaluate_reconstruction_readiness",
|
||||
"evaluate_reconstruction_coverage",
|
||||
"execute_node_characterization",
|
||||
"prepare_node_characterization",
|
||||
"query_reconstruction_coverage",
|
||||
"run_controlled_replay",
|
||||
"trace_application_feature",
|
||||
"trace_javascript_semantics"
|
||||
@@ -364,6 +384,12 @@
|
||||
"version": "1",
|
||||
"capabilities": ["capture_browser_scenario"]
|
||||
},
|
||||
{
|
||||
"id": "rea-playwright-electron-active",
|
||||
"name": "REA Playwright active Electron observation provider",
|
||||
"version": "1",
|
||||
"capabilities": ["capture_electron_scenario"]
|
||||
},
|
||||
{
|
||||
"id": "rea-v8-inspector",
|
||||
"name": "REA passive Node/Electron V8 Inspector provider",
|
||||
@@ -418,170 +444,8 @@
|
||||
"configuration": "detect-only"
|
||||
}
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"id": "analysis_snapshot",
|
||||
"version": 2
|
||||
},
|
||||
{
|
||||
"id": "artifact_extraction",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "artifact_graph",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "browser_scenario",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "browser_scenario_capture",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "browser_scenario_diff",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "browser_target_list",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "electron_page_inspection",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "electron_target_list",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "evidence",
|
||||
"version": 2
|
||||
},
|
||||
{
|
||||
"id": "evidence_bundle",
|
||||
"version": 2
|
||||
},
|
||||
{
|
||||
"id": "evidence_completion_ledger",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "evidence_completion_manifest",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "investigation_run",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "investigation_run_summary",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "investigation_workspace",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "javascript_application_analysis",
|
||||
"version": 2
|
||||
},
|
||||
{
|
||||
"id": "javascript_application_graph",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "javascript_application_version_comparison",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "javascript_feature_trace",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "javascript_runtime_observation",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "javascript_runtime_target_list",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "javascript_semantic_graph",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "javascript_semantic_query",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "javascript_semantic_trace",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "managed_artifact_inspection",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "managed_member_comparison",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "managed_member_inspection",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "managed_native_boundary_inspection",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "managed_native_verification",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "process_capture",
|
||||
"version": 4
|
||||
},
|
||||
{
|
||||
"id": "reconstruction_verification",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "residual_unknown",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "web_bundle_analysis",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "web_capture_diff",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "web_mcp_discovery",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "web_observation_session",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "web_page_inspection",
|
||||
"version": 2
|
||||
},
|
||||
{
|
||||
"id": "web_screenshot",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "web_screenshot_diff",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"cli": {
|
||||
"primary_count": 66,
|
||||
"primary_count": 75,
|
||||
"commands": [
|
||||
"analyze",
|
||||
"analyze-javascript-application",
|
||||
@@ -589,9 +453,10 @@
|
||||
"build-reconstruction-obligation-ledger",
|
||||
"capabilities",
|
||||
"capture-browser-scenario",
|
||||
"capture-electron-scenario",
|
||||
"capture-native-ui-scenario",
|
||||
"capture-process",
|
||||
"capture-web-screenshot",
|
||||
"commit-reconstruction-coverage",
|
||||
"compare",
|
||||
"compare-application-versions",
|
||||
"compare-javascript-export-shapes",
|
||||
@@ -600,11 +465,12 @@
|
||||
"compare-source-to-bundle",
|
||||
"compare-web-captures",
|
||||
"compare-web-screenshots",
|
||||
"decode-interface-builder",
|
||||
"decompile",
|
||||
"demangle-swift",
|
||||
"discover-webmcp-tools",
|
||||
"doctor",
|
||||
"evaluate-reconstruction-readiness",
|
||||
"evaluate-reconstruction-coverage",
|
||||
"evidence-export",
|
||||
"evidence-import",
|
||||
"execute-node-characterization",
|
||||
@@ -614,30 +480,35 @@
|
||||
"import-reference-source",
|
||||
"inspect",
|
||||
"inspect-artifact",
|
||||
"inspect-asset-catalog",
|
||||
"inspect-electron-page",
|
||||
"inspect-keyed-archive",
|
||||
"inspect-macho",
|
||||
"inspect-managed-artifact",
|
||||
"inspect-managed-members",
|
||||
"inspect-managed-native-boundaries",
|
||||
"inspect-native-api",
|
||||
"inspect-native-data-type",
|
||||
"inspect-native-dispatch-metadata",
|
||||
"inspect-native-instruction",
|
||||
"inspect-plist",
|
||||
"inspect-signature",
|
||||
"inspect-web-page",
|
||||
"instructions",
|
||||
"inventory-artifact",
|
||||
"investigate-versions",
|
||||
"list-architectures",
|
||||
"list-browser-targets",
|
||||
"list-electron-targets",
|
||||
"list-javascript-runtime-targets",
|
||||
"observe-javascript-runtime",
|
||||
"observe-native-ui",
|
||||
"observe-web-session",
|
||||
"plan-managed-runtime-correlation",
|
||||
"policy",
|
||||
"prepare-node-characterization",
|
||||
"project-managed-application-graph",
|
||||
"providers",
|
||||
"query-reconstruction-coverage",
|
||||
"reconcile-javascript-runtime",
|
||||
"resolve-native-call-targets",
|
||||
"run-controlled-replay",
|
||||
"run-replay-machine",
|
||||
"search",
|
||||
@@ -645,6 +516,8 @@
|
||||
"trace",
|
||||
"trace-application-feature",
|
||||
"trace-javascript-semantics",
|
||||
"trace-native-ui-action",
|
||||
"trace-native-values",
|
||||
"uninstall",
|
||||
"upgrade",
|
||||
"verify-managed-native-boundaries",
|
||||
@@ -659,17 +532,15 @@
|
||||
},
|
||||
"runtime_catalog": {
|
||||
"counts": {
|
||||
"cli_commands": 66,
|
||||
"mcp_tools": 110,
|
||||
"mcp_prompts": 6,
|
||||
"mcp_resources": 2,
|
||||
"mcp_resource_templates": 11
|
||||
"cli_commands": 75,
|
||||
"mcp_tools": 122,
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "9a033ddd40fcb8273365cc895bbf75d943bf1c5c8c2ee1eb9c4b3494064aaf58",
|
||||
"prompts_sha256": "f6da3ad38a1000c11364d1710f122ac4990551f04efd8c53ffb2e819b5cf5707",
|
||||
"resources_sha256": "339a984ea8cd8be5f234adf24395bf0295258d990492005cf6fe1f293d12cd46",
|
||||
"combined_sha256": "e8cac51f2f17a5b9a088c3ff773473c4897823401481fc0875a28fe7daf8eee0"
|
||||
"tools_sha256": "1a32398486b1e65f50a1a81de2d8867e6a4c296c165bf9994e53034b466c41b4",
|
||||
"prompts_sha256": "fa889fc671185ac9c968c6dc8309cf609e709cdc837dbbf39b7cdfadb84ac2d7",
|
||||
"combined_sha256": "a87e48bc626cb4361f494c074e71852304bfedee17fbba89976d32bb6244f230",
|
||||
"providers_sha256": "c5a88a49990fafc8244972ee8ff543fc19995eb79db960da55f6a311e15340d1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+59
-52
@@ -1,20 +1,20 @@
|
||||
# Static-analysis provider evaluation
|
||||
|
||||
Status: the Ghidra read-only analysis provider is shipped on Linux x64 and has
|
||||
an experimental Windows x64 P0 for approved native PE applications. It
|
||||
validates an exact bring-your-own Ghidra 12.1.2/JDK 21 environment, resolves a
|
||||
Status: the Ghidra read-only analysis provider is shipped on Linux x64 and macOS x64/arm64 with matching native tools, and has
|
||||
an experimental Windows x64 P0 for approved native PE applications. The provider
|
||||
validates an exact bring-your-own Ghidra 12.1.4/JDK 21 environment, resolves a
|
||||
provider/version/profile commitment, runs one isolated read-only headless
|
||||
import, and publishes 19 operation-level capabilities after the authenticated
|
||||
import, and publishes 22 operation-level capabilities after the authenticated
|
||||
post-analysis handshake.
|
||||
|
||||
The provider-neutral target, provider registry, deterministic target binding,
|
||||
analysis-profile commitment, Evidence provenance, snapshot v2, and bounded
|
||||
analysis-profile commitment, Evidence provenance, snapshot caching, and bounded
|
||||
provider-process lifecycle foundations are implemented. The Ghidra launcher,
|
||||
packaged Java bridge, doctor/setup projection, bounded client lifecycle, and
|
||||
multi-target real Linux verifier, curated Windows CI, and controlled real
|
||||
Windows verifier are also implemented. Program identity,
|
||||
procedure/string/symbol inventory, memory blocks, address/name and
|
||||
containing-procedure resolution, bounded search, function metadata,
|
||||
containing-procedure resolution, complete inventory search, function metadata,
|
||||
decompilation, assembly, resolved calls, typed references, xrefs, CFG, and
|
||||
function dossiers are admitted. A provider is not a drop-in replacement for
|
||||
Hopper: every capability is mapped explicitly, with truthful `unavailable`,
|
||||
@@ -34,14 +34,14 @@ follow.
|
||||
- Start with bring-your-own Ghidra and a compatible Java runtime. Setup must not
|
||||
install or upgrade Java.
|
||||
- Run Ghidra headlessly in an owned process with a private temporary project,
|
||||
bounded startup/analysis/request deadlines, cancellation, and cleanup.
|
||||
startup and cleanup deadlines, caller cancellation, and complete replies.
|
||||
- Prefer a packaged Java bridge loaded through Ghidra's script path. PyGhidra
|
||||
remains useful for prototypes but is not a mandatory production dependency.
|
||||
- Treat authenticated `ping` and `shutdown` as lifecycle proof only. Inventory,
|
||||
xref, CFG, and decompilation claims require separately admitted operation
|
||||
contracts and real-provider conformance.
|
||||
- Implement read-only inventories, assembly, decompilation, function metadata,
|
||||
calls, references, containment, and bounded search first.
|
||||
calls, references, containment, and complete inventory search first.
|
||||
- Report GUI cursor/navigation and persistent mutation operations as unavailable
|
||||
until their semantics and project ownership are explicitly designed.
|
||||
- Verify real claims on Linux with at least two distinct source-owned binaries.
|
||||
@@ -51,7 +51,7 @@ follow.
|
||||
|
||||
## Shipped foundation boundary
|
||||
|
||||
`GHIDRA_INSTALL_DIR` must identify an extracted official 12.1.2 release;
|
||||
`GHIDRA_INSTALL_DIR` must identify an extracted official 12.1.4 release;
|
||||
optional `JAVA_HOME` must identify a 64-bit full JDK 21, otherwise doctor probes
|
||||
`java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Linux accepts x86, x86-64,
|
||||
ARM, and ARM64 ELF, PE, and Mach-O executable targets. Windows P0 accepts only
|
||||
@@ -61,9 +61,9 @@ other formats, and other architectures before launch.
|
||||
The launcher creates one ephemeral runtime root with project,
|
||||
home/cache/config/data/temp, logs, descriptor, endpoint, target snapshot, and
|
||||
ownership manifest.
|
||||
It passes `-readOnly`, `-deleteProject`, a 300-second per-file analysis limit,
|
||||
two CPUs, and a 2 GiB heap; inherited Java option injection variables are
|
||||
cleared. On Linux, the mode-0600 descriptor carries the random token without
|
||||
It passes `-readOnly` and `-deleteProject`, and uses Ghidra's default analysis
|
||||
and resource settings; inherited Java option injection variables are cleared.
|
||||
On Linux, the mode-0600 descriptor carries the random token without
|
||||
exposing it in argv or environment and the Java bridge binds a mode-restricted
|
||||
Unix socket. Windows uses authenticated IPv4 loopback with a strict token-free
|
||||
endpoint record. Both transports report actual
|
||||
@@ -83,43 +83,50 @@ capability from a successful import.
|
||||
|
||||
## Admitted inventory semantics
|
||||
|
||||
| Concern | Ghidra contract |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Program identity | One `analyzeHeadless` import produces exactly one Program; `list_documents` therefore returns exactly one name. |
|
||||
| Addresses | Default memory uses lowercase `0x` hexadecimal. Non-default and external spaces use `<percent-encoded-space>:0x<hex>` and remain round-trippable. The handshake commits image base and default address-space name. |
|
||||
| Symbols | `list_names` includes address-bearing memory and external symbols, including dynamic symbols, while excluding variable and no-address namespace records. Each item reports primary, dynamic, external, symbol type, and source facts. |
|
||||
| Procedures | Both non-external and external functions are listed. A local thunk remains distinct from its resolved target; exact and qualified name lookup fails on ambiguity rather than guessing. |
|
||||
| Strings | Only Ghidra-defined string `Data` is observed. Items report charset, byte length, and whether a required null terminator is missing. The API cannot distinguish a present terminator from fixed/Pascal layouts when no terminator is missing, so that state is named `present_or_not_required`. |
|
||||
| Memory | Memory-block end addresses are exclusive. Read/write/execute, initialization, overlay, address space, and image base are direct Ghidra observations. |
|
||||
| Pagination | List limits are 500 and search limits are 100. Pages commit exact totals and advancing offsets; an inventory above one million items fails rather than returning a partial result labeled exhaustive. |
|
||||
| Search | Literal search scans the immutable inventory with a 1,000,000-unit cumulative work budget. Regex uses a conservative finite Java-regex subset with 10,000 static paths, 4,096 UTF-16 code units per candidate, and the same cumulative budget. Exceeding a budget fails explicitly; returned values identify truncation. |
|
||||
| Analysis state | The socket is exposed only after default auto-analysis. A 300-second analysis timeout fails target opening; ordinary established requests have a 10-second client deadline and every response has a 1 MiB ceiling. |
|
||||
| Concern | Ghidra contract |
|
||||
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Program identity | One `analyzeHeadless` import produces exactly one Program; `list_documents` therefore returns exactly one name. |
|
||||
| Addresses | Default memory uses lowercase `0x` hexadecimal. Non-default and external spaces use `<percent-encoded-space>:0x<hex>` and remain round-trippable. The handshake commits image base and default address-space name. |
|
||||
| Symbols | `list_names` includes address-bearing memory and external symbols, including dynamic symbols, while excluding variable and no-address namespace records. Each item reports primary, dynamic, external, symbol type, and source facts. |
|
||||
| Procedures | Both non-external and external functions are listed. A local thunk remains distinct from its resolved target; exact and qualified name lookup fails on ambiguity rather than guessing. |
|
||||
| Strings | Only Ghidra-defined string `Data` is observed. Items report charset, byte length, and whether a required null terminator is missing. The API cannot distinguish a present terminator from fixed/Pascal layouts when no terminator is missing, so that state is named `present_or_not_required`. |
|
||||
| Memory | Memory-block end addresses are exclusive. Read/write/execute, initialization, overlay, address space, and image base are direct Ghidra observations. |
|
||||
| Inventory | Procedure, symbol, and string listings plus searches return the complete matching collection in one response; callers do not provide offsets or result-count limits. |
|
||||
| Search | Literal and Java-regex searches scan the complete immutable inventory and return all matching entries inline. Search waits for a reply or caller cancellation; there are no caller-supplied offsets or result-count limits. |
|
||||
| Analysis state | The socket is exposed only after default auto-analysis completes. Established operations wait for their reply or caller cancellation; there is no fixed response-size ceiling. |
|
||||
|
||||
## Admitted function-analysis semantics
|
||||
|
||||
| Concern | Ghidra contract |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Each native decompile has a 30-second deadline; the socket leaves a bounded 35-second projection window. External functions or functions without bodies return `null`; timeout, cancellation, and native failure remain distinct. |
|
||||
| Serialization | A bounded FIFO admits at most 32 active-plus-queued requests and sends one Program request at a time. Queue wait counts against the caller's deadline, and queued cancellation is prompt. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
|
||||
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
|
||||
| Assembly and pseudocode | Assembly is bounded Ghidra Listing text; pseudocode is bounded Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
|
||||
| Instruction fast path | `read_function_instructions` returns one offset-paginated raw Listing window without invoking the decompiler or whole-program name/string inventories. It reports scan work, exact-or-unknown totals, truncation, and continuation metadata. |
|
||||
| Calls and references | Callers/callees contain only resolved functions. Reference edges preserve exact ReferenceManager type and call/jump/data/read/write/indirect/computed/conditional/terminal/external facts. Targetless computed flow remains unknown. Synthetic entry-point references without actionable memory sources are omitted explicitly. |
|
||||
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
|
||||
| Bounds | Direct assembly fails above 100,000 instructions or the wire ceiling. Dossiers scan at most 5,000 requested instructions, return bounded independent pages, mark unknown totals after an incomplete scan, and paginate pseudocode by Unicode code points. |
|
||||
| Concern | Ghidra contract |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
|
||||
| Serialization | A FIFO sends one Program request at a time. Caller cancellation removes queued work promptly and is passed to active socket waits. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
|
||||
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
|
||||
| Assembly and pseudocode | Assembly is complete Ghidra Listing text; pseudocode is Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
|
||||
| Instruction fast path | `read_function_instructions` returns every raw Listing instruction for the requested function without invoking the decompiler or whole-program name/string inventories. |
|
||||
| Calls and references | Callers/callees contain only resolved functions. Reference edges preserve exact ReferenceManager type and call/jump/data/read/write/indirect/computed/conditional/terminal/external facts. Targetless computed flow remains unknown. Synthetic entry-point references without actionable memory sources are omitted explicitly. |
|
||||
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
|
||||
| P-code value flow | Function dossiers include up to 3,000 high-p-code operations, at most 64 inputs per operation, and 12,000 def-use edges. Results identify memory reads/writes, branches, and calls by p-code opcode, and mark decompiler-dead operations. Large results report truncation, an omitted-operation lower bound, and known omitted-input/edge counts. The dossier is an intra-function graph; `trace_native_values` composes derived parameter/argument and return/output dependencies across resolved calls. It does not resolve memory aliasing, call side effects, runtime behavior, persistent state, or business meaning. |
|
||||
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
|
||||
|
||||
`npm run verify:ghidra` compiles the versioned C oracles into x86-64 debug and
|
||||
stripped ELF, AArch64 ELF, x86-64 PE, and x86-64 Mach-O targets. It proves all
|
||||
19 operations, external functions, resolved thunks, exports, stripped-name
|
||||
`npm run verify:ghidra` compiles the versioned C oracle into debug and stripped
|
||||
host-native targets (x86-64 ELF on Linux x64 or Mach-O on macOS), plus a native
|
||||
DWARF 4 type-layout object. It proves the admitted operations, external functions, resolved thunks, exports, stripped-name
|
||||
behavior, direct and targetless indirect calls, typed references, strings/xrefs,
|
||||
multi-block CFG, semantic enhanced workflows, cancellation, deadlines,
|
||||
multi-block CFG, bounded p-code def-use/effect links, semantic enhanced workflows, cancellation, startup deadlines,
|
||||
serialized concurrency, malformed-target rejection, profile identity, and
|
||||
process/project cleanup against real Ghidra 12.1.2. Unit fixtures separately
|
||||
cover analysis/decompile timeouts, process exit, queue saturation, and malformed
|
||||
wire output.
|
||||
process/project cleanup against real Ghidra 12.1.4. Unit fixtures separately
|
||||
cover startup deadlines, process exit, queued cancellation, and malformed wire
|
||||
output.
|
||||
|
||||
`npm run verify:ghidra:cross-format` adds AArch64 ELF, x86-64 PE, and x86-64
|
||||
Mach-O target coverage. It requires `clang`, LLD, and `lld-link`; set
|
||||
`REA_CLANG`, `REA_LLD`, or `REA_LLD_LINK` to select them. The verifier checks
|
||||
these tools before compilation. Keeping this matrix separate lets Linux
|
||||
host/provider acceptance run with only the host compiler.
|
||||
|
||||
`npm run verify:ghidra:windows` generates a deterministic source-owned native
|
||||
x86-64 PE application, proves all 19 operations through the production Windows
|
||||
x86-64 PE application, proves the Windows P0 operations through the production Windows
|
||||
launcher and loopback transport, checks target/snapshot/import SHA-256 linkage,
|
||||
and requires project, endpoint, process, and runtime cleanup. Passing that lane
|
||||
proves only the documented P0 boundary, not the remaining Windows security
|
||||
@@ -130,7 +137,7 @@ gates.
|
||||
`src/process/` now provides the mechanisms that a long-lived Hopper or Ghidra
|
||||
adapter genuinely shares: POSIX run-token-authenticated process-group ownership,
|
||||
ephemeral temporary runtime roots, one absolute startup deadline, correlated
|
||||
request timeout/cancellation cleanup, bounded stdout and stderr retention with
|
||||
request cancellation and lifecycle-deadline cleanup, bounded stdout and stderr retention with
|
||||
exact byte counts, process-exit diagnostics, and bounded TERM-to-KILL shutdown.
|
||||
Reusable fixtures exercise exit, timeout, cancellation, graceful termination,
|
||||
forced termination, double-close, spawn failure, and resource release. Windows
|
||||
@@ -145,12 +152,12 @@ only the generic process mechanisms.
|
||||
|
||||
## Shortlist
|
||||
|
||||
| Provider | License / automation surface | What it brings | REA fit and blockers |
|
||||
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Broad static analysis, many processors and formats, scripting, and project/database workflows | Read-only analysis shipped: exact BYO checks, isolated state, bounded process and queue, packaged bridge, 19 admitted inventory/function operations, and real ELF/PE/Mach-O x86-64 plus AArch64 conformance. GUI and mutation semantics remain intentionally unavailable. |
|
||||
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
|
||||
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
|
||||
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
|
||||
| Provider | License / automation surface | What it brings | REA fit and blockers |
|
||||
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Broad static analysis, many processors and formats, scripting, and project/database workflows | Read-only analysis shipped: exact BYO checks, isolated state and provider lifecycle, serial API queue, packaged bridge, 19 inventory/function operations, and real ELF/PE/Mach-O x86-64 plus AArch64 conformance. GUI and mutation semantics remain intentionally unavailable. |
|
||||
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
|
||||
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
|
||||
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
|
||||
|
||||
## Recommended order
|
||||
|
||||
@@ -170,7 +177,7 @@ Before implementation, an adapter proposal must provide:
|
||||
|
||||
- a capability matrix covering supported, unsupported, and degraded results;
|
||||
- provider identity, version, analysis profile, target digest, authority,
|
||||
limitations, and deterministic locations in every Evidence v2 record;
|
||||
limitations, and deterministic locations in every Evidence record;
|
||||
- bounded subprocess or library lifetime, cancellation, timeouts, and cleanup;
|
||||
- actionable local diagnostics that retain paths, digests, mismatch locations,
|
||||
and provider metadata while redacting credentials, authorization headers,
|
||||
@@ -184,9 +191,9 @@ prematurely claiming equivalence or redistribution support.
|
||||
|
||||
## Primary-source notes
|
||||
|
||||
- Ghidra 12.1.2 documents headless batch mode and its Java runtime requirements
|
||||
in its [release-specific Getting Started guide](https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.2_build/GhidraDocs/GettingStarted.md);
|
||||
the [official release](https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.2_build)
|
||||
- Ghidra 12.1.4 documents headless batch mode and its Java runtime requirements
|
||||
in its [release-specific Getting Started guide](https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md);
|
||||
the [official release](https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build)
|
||||
supplies the corresponding distribution and checksums.
|
||||
- Rizin lists supported formats, architectures, tools, and `rzpipe` bridges in
|
||||
its [repository README](https://github.com/rizinorg/rizin); `rz-pipe` documents
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# Reconstruction obligation ledgers
|
||||
|
||||
`build_reconstruction_obligation_ledger` turns authenticated Evidence v2 records
|
||||
into a versioned, deterministic list of reconstruction claims. The same workflow
|
||||
`build_reconstruction_obligation_ledger` turns authenticated Evidence records
|
||||
into a deterministic list of reconstruction claims. The same workflow
|
||||
is available as:
|
||||
|
||||
- MCP tool: `build_reconstruction_obligation_ledger`
|
||||
- CLI command: `rea build-reconstruction-obligation-ledger`
|
||||
- MCP resource:
|
||||
`rea://evidence/{evidenceId}/reconstruction-obligation-ledger`
|
||||
|
||||
The ledger is conservative by design. Static Application Graph facts create
|
||||
candidate obligations; they do not prove runtime or process behavior. A required
|
||||
@@ -29,7 +27,6 @@ never claims closure:
|
||||
rea build-reconstruction-obligation-ledger \
|
||||
'{
|
||||
"evidence_bundle": {
|
||||
"bundle_version": 2,
|
||||
"artifacts": [],
|
||||
"providers": [],
|
||||
"environments": [],
|
||||
@@ -40,18 +37,15 @@ rea build-reconstruction-obligation-ledger \
|
||||
},
|
||||
"reviewed_obligations": [],
|
||||
"manifest": {
|
||||
"schema_version": 1,
|
||||
"bindings": [],
|
||||
"contradictions": []
|
||||
},
|
||||
"limits": { "max_obligations": 100 },
|
||||
"page": { "offset": 0, "limit": 50 }
|
||||
}
|
||||
}' \
|
||||
--json
|
||||
```
|
||||
|
||||
For a real investigation, export an Evidence bundle from the session and replace
|
||||
the empty bundle. Application Graph and Process Capture v4 records generate
|
||||
the empty bundle. Application Graph and process capture records generate
|
||||
conservative candidates automatically. Add reviewed obligations when a required
|
||||
claim is not represented by those sources.
|
||||
|
||||
@@ -326,8 +320,6 @@ close a `packaged-process` obligation. A static-only source cannot close a
|
||||
runtime or process claim. Marking a claim `blocked` or `out-of-scope` records the
|
||||
disposition but does not silently count it as verified.
|
||||
|
||||
Pagination changes only `page` and the returned `obligations` slice. Every page
|
||||
retains the same `ledger_id`, `closure_digest`, full-ledger summaries, reports,
|
||||
ownership graph, dependency graph, Evidence links, and limitations. Consumers
|
||||
should follow `next_offset` until it is `null`, and should reject pages whose
|
||||
identity or closure digest changes mid-read.
|
||||
The tool returns every obligation inline. Its `ledger_id` and `closure_digest`
|
||||
identify the complete result, including summaries, reports, ownership and
|
||||
dependency graphs, Evidence links, and limitations.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Reconstruction readiness conformance
|
||||
|
||||
Run the complete source-owned native, JavaScript CLI, Electron, Evidence,
|
||||
comparison, obligation, replay, CLI, and MCP journey from a clean checkout:
|
||||
Run the source-owned native, JavaScript CLI, Electron, Evidence, comparison,
|
||||
obligation, replay, and cleanup conformance verifier from a clean checkout:
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
@@ -10,10 +10,10 @@ npm run verify:readiness
|
||||
|
||||
The command builds the redistributable native conformance fixtures, inventories
|
||||
the native executable through the public CLI, analyzes the generated JavaScript
|
||||
CLI and Electron fixture through both CLI and MCP, verifies exact advertised
|
||||
limits, evaluates the same nine-stage report through both surfaces, reads the
|
||||
full MCP report resource, and proves stale/tampered replay rejection. It prints
|
||||
one compact JSON summary.
|
||||
CLI and Electron fixtures through both CLI and MCP, evaluates an internal
|
||||
nine-stage conformance report, and proves stale/tampered replay rejection. The
|
||||
report evaluator is part of the repository verifier, not a public REA tool. It
|
||||
prints one compact JSON summary.
|
||||
|
||||
Set `REA_READINESS_REPORT_PATH` to retain the complete machine-readable report:
|
||||
|
||||
@@ -24,17 +24,11 @@ REA_READINESS_REPORT_PATH=readiness-report.json npm run verify:readiness
|
||||
CI runs this command on Linux, publishes the compact summary, and retains the
|
||||
full report as the `reconstruction-readiness` artifact.
|
||||
|
||||
## Public contracts
|
||||
## Project conformance contract
|
||||
|
||||
- CLI: `rea evaluate-reconstruction-readiness <json-or-file> --json`
|
||||
- MCP tool: `evaluate_reconstruction_readiness`
|
||||
- Full MCP resource:
|
||||
`rea://evidence/{evidenceId}/reconstruction-readiness-report`
|
||||
|
||||
The MCP tool result contains only report identity, status, summary, metrics, and
|
||||
the resource URI. The full Evidence bundle, stage records, comparisons,
|
||||
contradictions, obligation ledger, and replay inputs stay in the retained
|
||||
resource instead of consuming the tool-response context.
|
||||
The verifier runs as `npm run verify:readiness`; it is not exposed as a public
|
||||
CLI command or MCP tool. The synthetic report input and evaluator exist to
|
||||
check project-owned conformance fixtures and fail-closed report behavior.
|
||||
|
||||
## Required stages
|
||||
|
||||
@@ -51,7 +45,7 @@ resource instead of consuming the tool-response context.
|
||||
| `export-replay` | CLI/MCP parity, process/structured failure parity, deterministic replay, tamper rejection, and stale-input rejection |
|
||||
|
||||
Every stage and required check must occur exactly once and link to Evidence in
|
||||
the embedded Evidence v2 bundle. `fail`, `unknown`, `unsupported`, `truncated`,
|
||||
the embedded Evidence bundle. `fail`, `unknown`, `unsupported`, `truncated`,
|
||||
and `skipped` never aggregate to `pass`.
|
||||
|
||||
## Fail-closed rules
|
||||
|
||||
+8
-3
@@ -9,9 +9,10 @@ Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. The
|
||||
first six have documented local MCP configuration boundaries and can be updated
|
||||
additively; Devin is reported but left unchanged.
|
||||
|
||||
The Ghidra foundation supports Linux x64 with exact Ghidra 12.1.2 and a 64-bit
|
||||
full JDK 21. An experimental Windows x64 P0 admits approved native x86-64 PE
|
||||
applications through the same 19-operation Java bridge. Doctor validates those coordinates; approved Linux/macOS setup propagates them
|
||||
The Ghidra foundation supports Linux x64 with exact Ghidra 12.1.4 and a 64-bit
|
||||
full JDK 21. macOS is not an admitted Ghidra host. An experimental
|
||||
Windows x64 P0 admits approved native x86-64 PE
|
||||
applications through the same 19-operation Java bridge. Doctor validates those coordinates; approved Linux setup propagates them
|
||||
to MCP registrations without installing or modifying either dependency. REA's
|
||||
packaged Java bridge then proves an isolated read-only headless import,
|
||||
post-analysis handshake, complete cleanup, ten admitted read-only inventory
|
||||
@@ -19,6 +20,10 @@ operations, and nine function-analysis operations. Real source-owned x86-64
|
||||
debug/stripped ELF, AArch64 ELF, PE, and Mach-O fixtures cover program,
|
||||
procedure, string, symbol, external/thunk, memory, resolution, search,
|
||||
decompilation, assembly, calls, typed references, xrefs, and CFG semantics.
|
||||
Function dossiers also expose bounded Ghidra high-p-code def-use and memory or
|
||||
control-flow effects, mark decompiler-dead operations, and report truncation
|
||||
plus known omitted counts. This is intra-function
|
||||
decompiler evidence; cross-function state flow remains unimplemented.
|
||||
Hosted Windows CI covers build, package, target admission, transport, and
|
||||
lifecycle seams; a controlled self-hosted workflow covers the real Ghidra P0
|
||||
claim.
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# Test suite audit
|
||||
|
||||
## Scope and priority
|
||||
|
||||
The expanded audit on 2026-10-04 reviewed every one of the 355 Vitest files
|
||||
present at its start: 82 domain/contracts, 56 application/composition, 147
|
||||
adapter/configuration/process/filesystem/CLI files, and 70 MCP/acceptance/
|
||||
conformance/evaluation files. Three review passes checked the proposed removals
|
||||
against existing stronger coverage and checked the final changes for lost
|
||||
assertions. The remaining suite has 351 files.
|
||||
|
||||
Prefer full real-provider E2E, then integration across schemas and transports,
|
||||
then real-data goldens. Remove lower-level duplication once the replacement
|
||||
asserts the same behavior. Keep distinct failure regressions that a successful
|
||||
E2E cannot exercise. Do not lower coverage thresholds to accommodate pruning.
|
||||
|
||||
## Changes made
|
||||
|
||||
| Previous test or assertion | Decision | Replacement or retained claim |
|
||||
| ----------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Asset application tests using fabricated `.car` bytes and injected utility JSON | Remove two plumbing cases | `verify:asset-catalog`: real actool → assetutil → CLI and stdio MCP, including digest, full metadata, pagination and invalid input |
|
||||
| Keyed archive positive binary/XML objects assembled with the same plist library as the reader | Remove two synthetic success cases | Captured Foundation XML plus reviewed complete object/reference graph golden; real Foundation binary/XML → CLI and stdio MCP |
|
||||
| Generic contract schema/annotation/example loops and output-map inventory comparisons | Consolidate into one SDK wire test | Assert exported schemas, guidance, effect annotations and valid examples on the actual advertised MCP catalog |
|
||||
| Ghidra function operation enum membership | Remove | Canonical catalog, provider handshake and actual operation roundtrips establish admission; malformed provider results remain covered |
|
||||
| Asset page assertion pinning `10_001` | Remove | Invalid-page rejection through real MCP; malformed utility data still covered |
|
||||
| Suggested prompt tool positions | Remove incidental ordering assertions | Keep authority, evidence, untrusted input and preparation constraints |
|
||||
| Package “acceptance” assertions pinning CI matrix/shard values and fake launcher command arrays | Remove configuration-shaped cases | Actual package/install verifier remains; retain a compact release verification-before-publish and registry-canary static guard |
|
||||
| Package failure classifier under acceptance | Move to package evaluation | Preserve all malformed/unrelated-failure rejection cases; do not call them E2E |
|
||||
| Native value graph checks conditional on success | Strengthen | Require success before pagination/ambiguity assertions; unexpected errors can no longer pass |
|
||||
| Native UI verifier treating permission denial as a positive pass | Separate outcomes | Default lane requires real capture/action success; opt-in permissions lane explicitly reports no positive E2E proof |
|
||||
|
||||
Ten previous test cases were removed or consolidated; three replacement
|
||||
Vitest cases cover the captured goldens and release guard. The two real artifact
|
||||
E2E scripts add product-level evidence outside the deterministic Vitest count.
|
||||
|
||||
## Exhaustive follow-up
|
||||
|
||||
The follow-up removes or consolidates another 31 test declarations, including
|
||||
three redundant test files. Parameterized declarations and executed cases have
|
||||
different counts; the figures above count source declarations. It also repairs
|
||||
assertions that could pass without exercising their stated behavior.
|
||||
|
||||
| Area | Cleanup | Stronger retained evidence |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| Configuration | Merge duplicate target/loader tests and preserve custom launcher, explicit provider and actionable error diagnostics | Independent malformed inputs; each capability flag is rejected separately |
|
||||
| Browser scenarios | Remove fake launch success and expression-selected artifact success | Real Chromium scenarios, actual artifacts, CLI capture and owned/external cleanup in `verify:browser` |
|
||||
| CLI policy | Remove duplicate confirmation helper cases | Compiled CLI proves project files stay unchanged until `--yes`; distinct interactive choices remain |
|
||||
| Provider capabilities | Remove numerical catalog freezes; consolidate Hopper byte-read discovery | Exact canonical capability identities, admission, mutation effects and detached descriptors |
|
||||
| Evidence ledger | Merge reordered-key checks into serialized bundle import; remove one-record quota claims | Atomic import, conflicts, residual revisions, actual large collections and byte regressions |
|
||||
| MCP batching and concurrency | Replace maximum concurrency and empty indistinguishable responses | Every distinct ordered batch result; concurrently gated requests complete in reverse order with matching payloads |
|
||||
| MCP inventories and graphs | Remove traversal/inventory call order and redundant catalog/prose checks | Exact complete cyclic graph, full inline inventories, canonical SDK catalog and all-handler roundtrips |
|
||||
| Authority and lifecycle | Attempt the refused Ghidra mutation; require successful setup, call and switch; close twice | Typed refusal with zero mutation dispatch, restored target calls and exact owned-resource cleanup |
|
||||
| Process and provider output | Remove incidental environment read and command option order/defaults | PID/ownership validation, exact disabled-policy failure and complete decoded response above 10 MiB |
|
||||
| Conformance and CI guards | Merge identical replay runs; remove duplicate missing-row fault, trivial hash/self equality and incidental worker/step counts | Distinct faulty evidence, digest order, actual test discovery, file existence and release/runner authority |
|
||||
| Pure schemas | Remove fabricated comparison-dimension injection and impossible long boundary success | Real artifact comparison and SDK coverage evaluation with returned boundary identity |
|
||||
|
||||
The removed long boundary selector test only parsed a 1,000-character request;
|
||||
workspace boundary identities admit at most 200 characters, so that positive
|
||||
could never represent a successful evaluation. The actual MCP evaluation stays
|
||||
covered. One unused ledger serialization helper was also retired after removing
|
||||
its only test consumer; no CLI or MCP contract changed.
|
||||
|
||||
No further immediately safe pruning was identified in the final review. Retained
|
||||
replacement candidates below are coverage gaps, rather than permission to delete
|
||||
unique tests. Test-only domain scaffolding also stays pending a separate decision
|
||||
about the corresponding source behavior; deleting those tests alone would leave
|
||||
unverified code and fail unused-export checks.
|
||||
|
||||
## Stronger replacements from the closing pass
|
||||
|
||||
Three more synthetic success cases were removed. Native value composition now
|
||||
runs through the production CLI and a separate stdio MCP subprocess with real
|
||||
Ghidra, using a source-built native fixture. It checks argument/parameter/return
|
||||
relationships, the fixture's global operand, authenticated Evidence, upstream
|
||||
and workflow analysis profiles, identical CLI/MCP graphs, discovery availability
|
||||
and successful MCP session closure. Ambiguity, cancellation, target changes,
|
||||
missing flow and bounded-work regressions remain in the focused suite.
|
||||
|
||||
That replacement found missing CLI workflow routing for `trace_native_values`
|
||||
and missing workflow profile classification. The routing and discovery now use
|
||||
the composed operation's actual provider prerequisites; workflow Evidence commits
|
||||
to the upstream Ghidra profile.
|
||||
|
||||
The lone hand-built client-document equality test is replaced by a real TOML
|
||||
filesystem transaction: reversed environment keys leave the original bytes
|
||||
unchanged and create no backup. A real ZIP entry with an application root longer
|
||||
than 5,000 characters replaces injected schema-only positive data. Inventory
|
||||
completeness tests partition actual scans into authenticated partial observations
|
||||
and require exact assembled results and every source citation, including more
|
||||
than 100 citations. These are consumer merge tests, not provider pagination
|
||||
claims. Replay aggregation now covers all four statuses and retains actual runner
|
||||
diagnostics; it does not claim to execute a real sandbox. Retained pixel-metric
|
||||
fixtures now write correct PNG chunk checksums.
|
||||
|
||||
The all-zero screenshot buffer is replaced by a real Chromium PNG of 9,467,940
|
||||
bytes, transported intact through CLI and stdio MCP and decoded by the production
|
||||
pixel comparator. The verifier configures the SDK client's receive buffer because
|
||||
its 10 MiB default is smaller than the inline JSON response. This does not claim
|
||||
large comparison-request transport coverage. A real SDK initialize request,
|
||||
captured during a successful production stdio handshake, replaces the JSON-RPC
|
||||
decoder's field-count-only oracle with exact field/value expectations.
|
||||
|
||||
The closing pass also removed assertions tied to internal call order, defaults,
|
||||
private cache sizes, duplicated polling and fabricated schema extensions. Unique
|
||||
malformed-input, security-capacity and cleanup regressions remain.
|
||||
|
||||
## Real process regression found
|
||||
|
||||
The full suite exposed a renderer ordering race. Publishing a terminal output
|
||||
journal entry could synchronously trigger a reactive resize before the preceding
|
||||
output was queued for rendering. Queue that output first. The existing real
|
||||
source-owned interactive PTY test now also requires the first resized frame to
|
||||
contain the output that triggered the resize, preserving the original event
|
||||
order rather than sorting or changing timestamps to conceal it.
|
||||
|
||||
## Coverage kept
|
||||
|
||||
Malformed archives, broken and cyclic references, unsupported metadata pointers,
|
||||
relative Objective-C method layouts, ambiguous call candidates, digest mismatch,
|
||||
cancellation, permission approval, bounded output and owned-resource cleanup
|
||||
remain covered. These cases provide evidence beyond a successful real-tool run.
|
||||
The hand-assembled relative method fixture stays until compiler-produced
|
||||
relative-list fixtures establish equivalent assertions.
|
||||
|
||||
## Remaining priorities
|
||||
|
||||
1. Extend the real Ghidra CLI/stdio MCP journey from value tracing to dispatch
|
||||
and UI handlers before pruning their hand-authored composition success cases.
|
||||
Native value tracing now establishes full transport parity.
|
||||
2. Real binary dispatch and nib → full product transports, with normalized
|
||||
semantic goldens. The compiler-backed readers already run in the new Apple
|
||||
CI lane; synthetic pointer/layout rejection tests remain useful.
|
||||
3. An approved interactive macOS runner for native UI capture/actions, including
|
||||
stdio MCP scenarios. Hosted runners without permissions cannot supply this
|
||||
proof. No arbitrary application's windows should be opened for this lane.
|
||||
4. Real macOS/Linux Ghidra CI runners with bring-your-own installations. This
|
||||
change adds Apple artifact CI without installing Java or Ghidra.
|
||||
5. Classify fake-runner replay/application acceptance accurately and replace
|
||||
repeated happy paths only after real sandbox journeys assert the same claims.
|
||||
The replay workflow's allow-unavailable mode must not imply executed replay.
|
||||
6. Replace repeated-array schema ceiling tests with real producer/transport
|
||||
regressions before deleting them. Existing integration tests do not yet
|
||||
establish their large-input guarantees.
|
||||
|
||||
Some static guards protect irreversible release authority rather than product
|
||||
behavior. Keep these few guards explicit and separate from E2E evidence.
|
||||
|
||||
## Verification after the expanded audit
|
||||
|
||||
- `npm run check:pr`: 351 files passed; 1,794 tests passed and two skipped.
|
||||
Typecheck, lint, formatting, unused-code and generated-document gates passed.
|
||||
- `npm run check:fast`: passed; 70 lint warnings and zero errors.
|
||||
- `npm run verify:browser`: actual Chromium observation and scenario CLI routes,
|
||||
retained artifacts, owned/external lifecycle checks and large CLI/stdio MCP PNG
|
||||
capture passed.
|
||||
- `npm run verify:ghidra`: macOS ARM64 with Ghidra 12.1.4/JDK 21 passed, including
|
||||
real native value tracing through CLI and stdio MCP: 171 nodes, 298 edges and
|
||||
six decompilations.
|
||||
- `npm run verify:asset-catalog`: real actool/assetutil → CLI and stdio MCP,
|
||||
complete records, pagination and malformed input checks passed.
|
||||
- `npm run verify:keyed-archive`: real Foundation binary/XML → CLI and stdio MCP,
|
||||
full graph golden, shared/cyclic identity and malformed scope checks passed.
|
||||
|
||||
- `npm run verify:fixtures`: seven source-built host fixtures, source/artifact
|
||||
digests and exhaustive inventory checks passed.
|
||||
|
||||
No Hopper windows were opened for this audit. The closing pass runs real Ghidra
|
||||
on macOS ARM64; earlier Hopper evidence remains separate. No new hosted CI success
|
||||
is claimed.
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
# Testing REA
|
||||
|
||||
Prefer evidence in this order: full end-to-end workflows with real production
|
||||
providers, integration tests across data/API boundaries, then golden regressions
|
||||
from real captured inputs. Keep focused module tests for distinct failure or
|
||||
semantic cases that these workflows cannot reliably reproduce. A test's path
|
||||
or suite name does not establish its behavioral depth.
|
||||
|
||||
Tests are grouped into Vitest projects so ownership, allowed dependencies and
|
||||
runtime cost are visible from their paths. Before pruning a test, identify the
|
||||
replacement scenario and its assertions; passing success journeys do not
|
||||
replace malformed input, cancellation, permission or cleanup coverage.
|
||||
|
||||
## Behavioral depths
|
||||
|
||||
| Depth | Location | What it proves |
|
||||
| ------------ | ----------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| Module | `src/**/*.test.ts` | One domain, service, or adapter through its narrow public surface |
|
||||
| Composition | `tests/composition/**` | Provider-neutral session and registry wiring; filesystem access is limited to fixture materialization |
|
||||
| Boundary | `tests/boundary/**` | Exactly one production filesystem, process, network, browser, CLI, or provider boundary |
|
||||
| MCP boundary | `tests/boundary/mcp/**` | MCP transport and tool-contract boundaries with isolated sessions and explicit cleanup |
|
||||
| Acceptance | `tests/acceptance/**` | A compiled CLI or MCP journey; injected providers still make it integration |
|
||||
| Conformance | `tests/conformance/**` | Shared provider contracts, parameterized by declared capabilities and explicit opt-outs |
|
||||
| Evaluation | `tests/evaluation/**` | Deterministic evaluator parsing, scoring, and report generation |
|
||||
|
||||
Colocated domain tests may import only their owning domain and inward
|
||||
dependencies. They do not construct sessions or servers and do not start
|
||||
subprocesses or network listeners. Colocated application tests exercise one
|
||||
service through explicit ports and recording adapters, never a CLI or MCP
|
||||
entrypoint. Composition tests may assemble provider-neutral sessions and
|
||||
registries but do not cross production filesystem, process, socket, or browser boundaries.
|
||||
Boundary tests cross one production boundary. Only acceptance tests assemble
|
||||
the complete runtime or invoke the compiled product surface.
|
||||
|
||||
Focused immutable builders and recording ports shared by one test family live
|
||||
beside their production owner as `src/**/*.fixture.ts`. They are typechecked
|
||||
with the suite and excluded from package builds; broader runtime and provider
|
||||
fixtures remain under `tests/fixtures/**`.
|
||||
|
||||
`tests/process-global/**` is reserved for cases with a demonstrated dependency
|
||||
on process-global state. Those tests run without file parallelism. Reusable,
|
||||
test-scoped fixtures live under `tests/support/**`; immutable source artifacts
|
||||
remain under `tests/fixtures/**`.
|
||||
The process-global Vitest configuration contract rejects new direct temporary-root
|
||||
creation outside the workspace seam and its narrowly documented boundary/package
|
||||
exceptions.
|
||||
|
||||
Real Hopper, Ghidra, browser, package, managed-code, and controlled-replay
|
||||
claims belong to their explicit `npm run verify:*` lanes. They are not inferred
|
||||
from mocks or folded into the deterministic local gate. Real model trials are
|
||||
manual; Vitest covers only deterministic evaluator logic.
|
||||
|
||||
## End-to-end, integration and golden evidence
|
||||
|
||||
Full E2E tests invoke the production command dispatcher and real providers,
|
||||
without fake launchers, runners or responses. `verify:keyed-archive` writes an
|
||||
actual Foundation binary archive, runs the CLI and a separate stdio MCP
|
||||
subprocess, checks parity and pagination, rejects an escaping path and checks
|
||||
an XML graph golden. `verify:asset-catalog` compiles source-owned colors with
|
||||
`actool`, invokes real `assetutil`, then checks CLI/MCP results, exact catalog
|
||||
digest, every raw metadata field, pagination and malformed input rejection.
|
||||
Neither artifact workflow requires Hopper or launches it. Both run in macOS CI.
|
||||
|
||||
MCP SDK transport tests with recording providers remain integration tests.
|
||||
They are useful for schema drift and failure projection but do not prove that
|
||||
Hopper, Ghidra or another substituted engine works. `verify:package` proves
|
||||
packaging/install behavior and fake-provider integration; use the corresponding
|
||||
real-provider lanes for engine claims. Real Apple dispatch and Interface
|
||||
Builder verifiers currently prove format integration through production readers.
|
||||
|
||||
Golden tests use immutable captured text inputs with producer/source provenance
|
||||
under `tests/fixtures/golden/`. Expected results are reviewed for the semantic
|
||||
claim; capture commands do not automatically approve new expected outputs.
|
||||
Do not call handcrafted utility output or synthetic binary builders real-data
|
||||
goldens. Keep unsupported binary layouts and malformed boundaries as targeted
|
||||
regressions until a real fixture establishes equivalent coverage.
|
||||
|
||||
`verify:browser` also captures a source-owned noise canvas as a real PNG above
|
||||
8 MiB through the CLI and stdio MCP, with complete byte/digest parity and real PNG
|
||||
decoding. Its SDK client explicitly permits the larger inline JSON response;
|
||||
this lane does not establish large image-comparison request transport coverage.
|
||||
|
||||
The [test suite audit](test-suite-audit.md) records the pruning decisions,
|
||||
replacement evidence and remaining priorities.
|
||||
|
||||
## Real-toolchain verification lanes
|
||||
|
||||
Each real-toolchain command must require only the host tools needed to prove
|
||||
its stated claim. Use a host-native fixture for host/provider acceptance, and
|
||||
place optional cross-target formats or platform-specific runners in separate
|
||||
commands. Check prerequisites before starting expensive work and name the
|
||||
missing command, target, and lane in any failure message. A lane must not imply
|
||||
that a host or target is covered when it was skipped.
|
||||
|
||||
| Ghidra lane | Supported runner/target | Additional local tools |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `npm run verify:ghidra` | Linux x64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites |
|
||||
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the Windows P0 fixture toolchain |
|
||||
|
||||
The host-native Ghidra lane also verifies native value tracing through the
|
||||
production CLI and a separate stdio MCP process. It compares complete dependency
|
||||
graphs, validates Evidence and upstream/workflow profiles, checks capability
|
||||
discovery, and closes the MCP session. No provider or transport is mocked.
|
||||
|
||||
The cross-format Ghidra lane also analyzes an optimized AArch64 ELF switch
|
||||
fixture. It checks the recovered case values against the source cases and
|
||||
requires unresolved table bounds or case mappings to remain visible as
|
||||
residual unknowns.
|
||||
|
||||
## Apple Interface Builder archives
|
||||
|
||||
`npm run verify:interface-builder` compiles the source-owned AppKit XIB into a
|
||||
real `.nib` with Xcode `ibtool`, wraps it in a temporary app bundle, and checks
|
||||
the decoded view hierarchy, outlet, action, evidence coverage, and truncation
|
||||
status. Storyboard compilation additionally requires an installed iOS platform.
|
||||
|
||||
Keep the provider-specific acceptance path independent from optional
|
||||
cross-compilers. Cross-format failures belong to the cross-format lane and must
|
||||
not make native host acceptance unavailable.
|
||||
|
||||
## Developer commands
|
||||
|
||||
`npm test` runs every deterministic project once. The narrower feedback loops
|
||||
are:
|
||||
|
||||
| Command | Scope |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `npm run test:fast` | Domain, service, adapter, composition, boundary, MCP boundary, and conformance projects |
|
||||
| `npm run test:local` | Changed and related tests without the build step; lightweight local feedback loop |
|
||||
| `npm run test:boundary` | Boundary, MCP boundary, and process-global projects |
|
||||
| `npm run test:mcp` | MCP boundary project only |
|
||||
| `npm run test:acceptance` | Complete CLI and MCP acceptance workflows |
|
||||
| `npm run test:changed` | Changed tests in non-serial projects via Vitest's import graph |
|
||||
| `npm run test:watch` | Changed domain, service, adapter, composition, boundary, MCP boundary, conformance, and evaluation tests in watch mode |
|
||||
| `npm run test:watch:all` | Changed tests from every deterministic project in watch mode |
|
||||
| `npm run check:changed` | Cached static checks followed by changed tests |
|
||||
| `npm run check:pr` | Static, generated-document, and complete deterministic PR gate |
|
||||
|
||||
Changed-test selection is a fast feedback aid, not release evidence. It can
|
||||
miss behavior connected through runtime registration, generated data, shell
|
||||
entrypoints, or other relationships that are absent from the import graph.
|
||||
Use `npm run check:pr` before handing off a contribution.
|
||||
|
||||
Local full-suite Vitest runs use up to two workers and schedule projects one
|
||||
at a time. Process, acceptance and process-global projects serialize their
|
||||
files to prevent competing lifecycle observations. CI retains its two-worker
|
||||
budget.
|
||||
The pure domain/contracts and recording-port service projects share one worker
|
||||
module context because their tests own no mutable runtime resources. MCP
|
||||
boundary files also share the immutable server module graph while creating and
|
||||
closing independent in-memory sessions. Adapter, composition, acceptance,
|
||||
process-global, and other boundary projects retain per-file isolation.
|
||||
`npm test`, `npm run docs:check`, and `npm run docs:generate` share
|
||||
repository-local locks and fail fast when the same class of command is already
|
||||
running. The `npm test` build is inside that lock. `check:pr` runs its test task
|
||||
before starting documentation validation, so TypeDoc does not compete with the
|
||||
full suite for memory.
|
||||
|
||||
Vitest and Node persistent compile caches are deliberately not enabled by
|
||||
default. To evaluate repeated local runs, opt in for both cold and warm
|
||||
measurements with an isolated cache:
|
||||
|
||||
```bash
|
||||
NODE_COMPILE_CACHE=.cache/node-compile npm test
|
||||
```
|
||||
|
||||
Do not report the warm result as a cold-suite improvement, and do not enable
|
||||
the cache in coverage or benchmark CI without first showing that its
|
||||
instrumentation remains equivalent.
|
||||
|
||||
## Coverage and timing
|
||||
|
||||
CI owns coverage. The aggregate floors are 65% statements, 60% branches, 60%
|
||||
functions, and 68% lines. `src/domain/**` must reach 80% statements, 75%
|
||||
branches, 75% functions, and 80% lines. `src/contracts/**` must reach 85%
|
||||
statements, 80% branches, 80% functions, and 85% lines. Thresholds are
|
||||
glob-specific rather than per-file and are never updated automatically.
|
||||
Coverage does not replace named boundary, acceptance, or real-provider scenario
|
||||
matrices.
|
||||
|
||||
CI runs four native Vitest shards without retries. Each shard emits a blob
|
||||
report; the merge job produces aggregate coverage plus JUnit and JSON timing
|
||||
reports, uploads them together, and writes the slowest files to the workflow
|
||||
summary. Static checks, documentation, build, package verification, and
|
||||
real-system lanes remain separate jobs so one kind of evidence cannot stand in
|
||||
for another.
|
||||
|
||||
The PR acceptance target is a median `npm run check:pr` wall time below three
|
||||
minutes across three warm-build runs on the benchmark host. Keep Vitest caches
|
||||
cold unless separately identified. A PR that touches packaging or real-system
|
||||
behavior also requires `npm run verify:package` and the applicable
|
||||
`verify:*` lanes.
|
||||
|
||||
## Apple native metadata and UI
|
||||
|
||||
`npm run verify:apple-dispatch` compiles Objective-C class/protocol and Swift
|
||||
conformance/vtable fixtures, inspects their bytes and repeats after stripping
|
||||
local symbols. It requires macOS and the host Xcode toolchain; targets are not
|
||||
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
|
||||
window and requires successful selected-window capture and approved actions.
|
||||
Permission denial fails the positive lane. `npm run verify:native-ui:permissions`
|
||||
allows a permission-boundary-only result and explicitly reports
|
||||
`positive_e2e: false`; it must not be reported as capture/action proof.
|
||||
Both commands reject a changed executable digest and clean up the fixture
|
||||
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
|
||||
for the exact ABI, authority, graph and observation boundaries.
|
||||
@@ -0,0 +1,94 @@
|
||||
# MCP tool design
|
||||
|
||||
Design tools around analyst tasks. A provider API is an implementation detail;
|
||||
expose it directly when it gives agents a useful, reusable capability that
|
||||
cannot be expressed through an existing contract. Do not impose caller-facing
|
||||
limits merely to make work fit an assumed agent budget. Keep real input-format,
|
||||
protocol, authority, and resource-safety constraints, and report their effects.
|
||||
|
||||
## Choose the tool shape
|
||||
|
||||
| Shape | Use it for | Contract should return |
|
||||
| -------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `inspect` | Facts about one explicit target, address, object, or resource | Relevant fields, source locations, and facet-level availability |
|
||||
| `search` / `list` | Finding candidate targets or entities | Stable ordering and useful result context; expose pagination when the result format or caller's query needs it |
|
||||
| `trace` | Relationships across code, metadata, UI resources, or observations | Typed edges, supporting evidence, and unresolved paths; identify any genuine traversal boundary |
|
||||
| `compare` | Two explicitly identified artifacts, versions, or evidence sets | Paired identity, comparable coverage, and deltas with evidence |
|
||||
| `workflow` | A distinct analyst outcome that benefits from composition inside REA | A useful inline result, contributing evidence, and partial/unavailable facets |
|
||||
| `observe` / `capture` / `replay` | A question that requires runtime behavior | Required authority, launch/attach behavior, real operational constraints, lifecycle, and cleanup status |
|
||||
|
||||
These are task shapes, not required prefixes. Name a tool for the action and
|
||||
object agents reason about; keep the name distinct from nearby alternatives.
|
||||
|
||||
## Prefer primitives; compose workflows
|
||||
|
||||
Start with a primitive when one call can report a reusable fact about one
|
||||
identified object or relationship, such as an instruction decode, a type
|
||||
layout, a reference, a dispatch target, or a resource graph. The same primitive
|
||||
should be useful across different applications or analyst questions. A
|
||||
format-specific decoder can still be a primitive when it describes a reusable
|
||||
format rather than one application's product behavior.
|
||||
|
||||
Add a workflow when repeated analysis shows that callers need the same
|
||||
multi-source result and REA can join the evidence without hiding important
|
||||
choices or uncertainty. A useful check is whether the workflow remains
|
||||
meaningful for different applications that share the relevant evidence types.
|
||||
If its purpose depends on one application's business rules or product concepts,
|
||||
keep that interpretation outside the general tool contract and expose the
|
||||
underlying evidence through reusable primitives.
|
||||
|
||||
## Decide whether to add a tool
|
||||
|
||||
1. State the user intent and the smallest result that answers it.
|
||||
2. Inspect the canonical tool contracts, current provider capabilities, and
|
||||
representative CLI/MCP traces. Record the nearest competing tool.
|
||||
3. Extend an existing contract when the intent and result are the same. Add a
|
||||
batch or composed workflow when it materially improves a demonstrated task;
|
||||
do not force agents through extra calls or collapse distinct tasks into one
|
||||
tool. Add a tool when it answers a distinct analyst question or has a
|
||||
materially different authority or result contract.
|
||||
4. Keep public tool names and result semantics provider-neutral. Put engine-
|
||||
specific parsing and protocol handling in adapters. Advertise exact support
|
||||
per provider; do not create parallel tools just because engines differ.
|
||||
5. Keep prompts optional and concise. They may point out useful tools, but must
|
||||
not prescribe a call sequence when the task can be answered directly.
|
||||
|
||||
Avoid opaque mode flags and mega-tools that combine unrelated discovery,
|
||||
execution, and mutation. Prefer a useful direct tool call over a tool sequence
|
||||
or model-authored loop when one call can answer the question.
|
||||
|
||||
## Define the contract
|
||||
|
||||
- Use strict object inputs with explicit required fields and enums. Add numeric
|
||||
bounds only when the format, protocol, authority, or a measured resource
|
||||
constraint requires them.
|
||||
- Return complete results by default. Make real pagination, truncation, partial
|
||||
failure, cancellation, and unavailable states explicit. Missing evidence is
|
||||
unknown, not empty or false.
|
||||
- Return task-oriented results inline with artifact identity, provider/version,
|
||||
addresses or resource paths, Evidence references, confidence, and relevant
|
||||
limitations. Do not require agents to fetch a resource or dereference an
|
||||
opaque Evidence link to understand a tool result.
|
||||
- Separate observed facts from derived and inferred edges. Cite the evidence
|
||||
supporting every important relationship; preserve unresolved edges.
|
||||
- Declare read-only, mutation, process, filesystem, network, and UI effects
|
||||
truthfully in the contract and permission boundary.
|
||||
- Keep provider-specific types out of provider-neutral domain and application
|
||||
layers. Normalize supported provider results without implying equal coverage.
|
||||
|
||||
## Implement and evaluate
|
||||
|
||||
- Put shared user workflows in the application layer; keep MCP translation in
|
||||
the server adapter and CLI behavior aligned with the same workflow.
|
||||
- Update the canonical contract inventory, output schemas, examples, generated
|
||||
catalog, and relevant docs together.
|
||||
- Verify valid, malformed, boundary, unavailable, cancellation, and partial
|
||||
results at the owning boundaries. Use real-provider checks for claims that
|
||||
depend on external analysis engines.
|
||||
- Evaluate representative broad and direct task prompts on the intended MCP
|
||||
host when tool selection is the question. Record the host, model-facing tool
|
||||
catalog, selected tool, arguments, errors, retries, and task outcome.
|
||||
Schema validity or word-overlap heuristics do not prove discoverability.
|
||||
|
||||
See [mcp-contracts.md](mcp-contracts.md) for shipped runtime behavior and
|
||||
[testing.md](testing.md) for test boundaries and verification lanes.
|
||||
@@ -12,12 +12,11 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
11,src/domain/errors.ts:ArtifactOperationError,Artifact inventory or extraction fails,"Artifact {operation} failed: {reason} at {logicalPath} (declared_sha256=...)",Exposes paths hashes and internal reason,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
|
||||
12,src/domain/errors.ts:EvidenceFileError,Evidence file operation fails,"Evidence bundle {operation} failed: {reason}",Internal reason token gives weak recovery,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
|
||||
13,src/domain/errors.ts:EvidenceIntegrityError,Evidence validation fails,Raw constructor message,May expose validation internals and lacks recovery,"Evidence is invalid or has changed. Recreate or re-import it, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Evidence state changed before the update completed. Refresh the current state and try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Missing IDs identify the session and retry; concurrent revision conflicts refresh current state.",implemented,focused assertions passed; full `npm run check` passed
|
||||
15,src/domain/errors.ts:ProcessCaptureError,Process capture fails,Raw constructor message,May expose process and cleanup internals,"Process capture could not complete. Check command access and limits, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"Analysis operation timed out after {timeoutMs}ms: {operation}",Exposes internal deadline and operation; weak recovery,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
|
||||
17,src/domain/errors.ts:HopperTimeoutError,Provider deadline expires,"Hopper did not respond within {timeoutMs}ms",Exposes provider and internal deadline,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
|
||||
18,src/domain/errors.ts:ProviderSelectionError,Deep-provider selection fails,"Analysis provider selection failed ({reason}) for {requestedProviderId}; candidates: {candidateIds}",Retains non-secret local selection context that needs a safe structured projection,Reason-specific guidance with stable candidate IDs and actionable rejection diagnostics,implemented,focused registry/CLI/MCP assertions passed; full `npm run check` passed
|
||||
19,src/domain/errors.ts:EvidenceLimitError,Evidence ledger limit reached,"Evidence ledger {limit} limit exceeded ({maximum})",Exposes internal storage model,"Evidence is too large for this session. Reduce the evidence set and try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
20,src/domain/errors.ts:AnalysisInputError,Parsed analysis input invalid,"Invalid analysis input for {operation}",Exposes operation identifier and lacks correction,"Analysis input is invalid. Check the arguments and try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
21,src/domain/errors.ts:AnalysisCancelledError,Caller cancels analysis,"Analysis operation was cancelled: {operation}",Exposes operation identifier,"Analysis was cancelled. Start it again when ready.",implemented,focused assertions passed; full `npm run check` passed
|
||||
22,src/domain/errors.ts:HopperCancelledError,Caller cancels provider request,Hopper operation was cancelled,Exposes provider name,"Analysis was cancelled. Start it again when ready.",implemented,focused assertions passed; full `npm run check` passed
|
||||
@@ -25,7 +24,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
24,src/server/toolResult.ts:successResult,Successful value violates public output schema,AnalysisOutputError: Analysis output does not match the tool contract,Exposes internal error tag and contract implementation,"Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`.",implemented,focused assertions passed; full `npm run check` passed
|
||||
24.5,src/server/registerOfficialTools.ts:capability unknown,Approved unknown tracking records an unavailable analysis,"TOOL_NAME is unavailable: PROVIDER_REASON",Exposes an internal tool identifier and provider diagnostic in persistent user-visible evidence,"The requested analysis is unavailable for the current target.",implemented,verified by focused MCP integration test
|
||||
24.6,src/application/ReferenceSourceImportEntries.ts:failed entry,A partial reference import contains an unreadable entry,Low-level reader diagnostic stored in entry limitations,Can persist filesystem or reader implementation details,"Code- and entry-specific safe guidance with a retry, limit, exclusion, or platform recovery step.",implemented,verified by exhaustive focused projection test
|
||||
24.7,src/server/registerSessionTools.ts:process residual,Capture records an approved residual unknown,SCOPE: RAW_REASON,Persists adapter diagnostic and implementation wording,"Was SCOPE behavior fully observed during capture?",implemented,verified by focused session MCP test
|
||||
24.7,src/server/registerSessionTools.ts:process residual,Capture records a residual unknown linked to capture Evidence,SCOPE: RAW_REASON,Persists adapter diagnostic and implementation wording,"Was SCOPE behavior fully observed during capture?",implemented,verified by focused session MCP test
|
||||
24.8,src/server/registerInvestigationTools.ts:changed behavior unknown,Changed-behavior workflow remains incomplete,"Changed behavior result EVIDENCE_ID remains STATUS",Persists internal evidence identifier,"Did both versions behave the same under a complete controlled replay?",implemented,verified by focused investigation MCP test
|
||||
24.9,src/server/registerInvestigationTools.ts:call path unknown,Call-path workflow remains incomplete,Call path START_ADDRESS → GOAL_ADDRESS is STATUS,Persists binary addresses and internal status wording,Can the requested call path be established from complete analysis?,implemented,verified by focused unresolved call-path MCP test
|
||||
24.91,src/server/registerInvestigationTools.ts:static runtime unknown,Static/runtime correlation remains incomplete,Static/runtime correlation result EVIDENCE_ID is STATUS,Persists internal evidence identifier,Does runtime behavior match the available static analysis?,implemented,verified by focused unresolved static/runtime MCP test
|
||||
@@ -33,10 +32,10 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
24.93,src/server/registerFunctionComparisonTool.ts:probe rationale,Function comparison is incomplete,Capture complete dossiers with equal providers and analysis limits.,Exposes provider architecture and gives ambiguous equality guidance,"Capture complete dossiers for both functions under the same target context and analysis limits.",implemented,verified by focused function-comparison MCP test
|
||||
24.95,incur:CLI argument validation,Caller omits or mistypes a command argument,VALIDATION_ERROR with raw Zod Details and fieldErrors including code/path/expected/received,Exposes validator representation and buries the useful correction,"REA could not read the command arguments. Check the named argument and its expected type, then try again.",implemented,verified by sanitizer unit tests and real dispatcher missing-argument test
|
||||
25,src/application/ReferenceSourceImport.ts:cancelled,Reference-source import is cancelled,Reference source import cancelled,No recovery timing guidance,"Reference source import was cancelled. Start it again when ready.",implemented,focused assertions passed
|
||||
26,src/application/ReferenceSourceImportPolicy.ts:invalid-limits,Configured import limits are not positive integers,Import limits must be positive integers,Does not tell user where recovery lies,"Reference source limits are invalid. Use positive integer limits, then try again.",implemented,focused assertions passed
|
||||
26,src/application/ReferenceSourceImportPolicy.ts:invalid-limits,Configured import limits are not positive integers,Import limits must be positive integers,Fixed importer ceilings prevented full source imports,Removed fixed byte, entry, depth, and path limits.,removed,verified by greater-than-10000 entry and greater-than-16-MiB regressions
|
||||
27,src/application/ReferenceSourceImportPolicy.ts:invalid-root,Source path cannot resolve or is not a directory,Reference source root could not be resolved / Reference source root is not a directory,Multiple low-level copies and no complete path check,"Reference source directory could not be opened. Check that the path exists, is readable, and points to a directory.",implemented,focused assertions passed
|
||||
28,src/application/ReferenceSourceImportPolicy.ts:policy,No approved root exists or requested root is outside policy,No approved reference source roots / Reference source root is outside approved roots,Does not identify safe configuration recovery,"Reference source directory is not approved. Add its directory to `REA_REFERENCE_ROOTS_JSON`, restart REA, then try again.",implemented,focused assertions passed
|
||||
29,src/application/ReferenceSourceImport.ts:io,Reader fails during bounded traversal,Raw reader error message,May expose filesystem details and inconsistent recovery,"Reference source files could not be read. Check directory permissions and try again.",implemented,focused assertions passed
|
||||
28,src/application/ReferenceSourceImportPolicy.ts:policy,No approved root exists or requested root is outside policy,No approved reference source roots / Reference source root is outside approved roots,Approval friction duplicates direct caller path selection,Removed the configured root allowlist and permission gate; caller-selected directories are validated and traversed directly.,removed,verified by source search and focused import tests
|
||||
29,src/application/ReferenceSourceImport.ts:io,Reader fails during traversal,Raw reader error message,May expose filesystem details and inconsistent recovery,"Reference source files could not be read. Check directory permissions and try again.",implemented,focused assertions passed
|
||||
30,src/application/ReferenceSourceImport.ts:parse,Historical graph validation fails,Raw thrown graph validation message,Can expose internal graph invariants,"Reference source could not be indexed. Check that the source tree is readable, then try again.",implemented,focused assertions passed
|
||||
31,src/application/ReferenceSourceImportTypes.ts:limit,Import reports terminal limit failure,No reachable constructor found,Declared state may be unreachable; false verification risk,Remove unreachable top-level limit variant; per-entry limit guidance remains caller-visible.,removed,"verified by typecheck, exhaustive projection test, and source search"
|
||||
32,src/application/Setup.ts:configureDetectedClients(backup),Agent configuration backup fails,"{clientName} configuration backup verification failed; no successful configuration was reported.",Exposes machine client key and obscures no-write guarantee,"Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup.",implemented,focused assertions passed
|
||||
@@ -73,8 +72,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
62,src/application/ProcessCli.ts:readJson size,Process scenario or evidence file exceeds 64 MiB,Process JSON input exceeds byte limit,Does not state limit or recovery,"Process input file is too large. Reduce it below 64 MiB, then try again.",implemented,verified by focused sparse-file CLI test
|
||||
63,src/application/ProcessCli.ts:readJson parse,Process scenario or evidence file is malformed JSON,Raw JSON parser exception through CLI framework,Can expose parser implementation and lacks recovery,"Process input file is not valid JSON. Repair the file, then try again.",implemented,verified by focused CLI test
|
||||
64,src/application/ProcessCli.ts:capture scenario parse,Scenario fails Process Capture schema,Raw Zod validation exception through CLI framework,Exposes schema internals and field paths without stable recovery,"Process scenario is invalid. Check its required fields and limits, then try again.",implemented,verified by focused CLI test
|
||||
65,src/application/ProcessCli.ts:legacy evidence,Comparison receives Process Capture v3 Evidence,"Process Capture v3 is unsupported. Re-run the scenario with this REA version using capture_process_scenario to produce Process Capture v4; captures cannot be upgraded because v4 requires new manifest and settlement evidence.",Long but accurately explains why recapture is required and gives exact tool recovery,Keep current copy,unchanged,verified by focused CLI test
|
||||
66,src/application/ProcessCli.ts:wrong evidence,Comparison receives valid Evidence from another workflow,Expected REA Process Capture v4 Evidence,Uses internal schema name and does not explain recovery,"Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.",implemented,verified by focused CLI test
|
||||
66,src/application/ProcessCli.ts:wrong evidence,Comparison receives valid Evidence from another workflow,Expected REA process capture Evidence,Uses internal schema name and does not explain recovery,"Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.",implemented,verified by focused CLI test
|
||||
67,src/application/ProcessCli.ts:unexpected,Unexpected parser/comparison/canonicalization failure escapes expected classifications,Raw thrown exception through CLI framework,Can expose stack or internal invariant,"Process command could not complete. Check the input files and run `rea doctor`, then try again.",implemented,verified by direct unexpected-cause projection test
|
||||
68,src/server/registerEvidenceTools.ts:object guard,Validated provider-tool input is unexpectedly not an object,Provider tool input was not an object,Exposes provider architecture and implies caller can fix impossible state,Remove the impossible duplicate guard and parse through the declared JSON-object schema.,removed,verified by typecheck and MCP integration tests
|
||||
69,src/server/registerEnhancedTools.ts:object guard,Validated enhanced-tool input is unexpectedly not an object,Validated enhanced input was not a JSON object,Exposes validation/JSON implementation detail,Remove the impossible duplicate guard and parse through the declared JSON-object schema.,removed,verified by typecheck and enhanced MCP integration tests
|
||||
|
||||
|
@@ -1,103 +1,102 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"ledger_id": "ecl_a56a75b91375c9c9501d1545fee9977e74f57dbd1bf413a370f9cd1ed26987bd",
|
||||
"ledger_id": "ecl_b01c0b28362b48128765d176113471a5a82637fca0acfcd3182971155942e0d4",
|
||||
"records": [
|
||||
{
|
||||
"claim_id": "managed.application-graph-projection",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_4d6821cdd212a77507dfbdf995fc8b086cd63b3f1d39dee4a2e12f62a6f0609d"
|
||||
"ev_b061a5a559acb3748cb9784c637f6693f74faeb564ba2b11d6fc32619e757459"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.decompiler-reconstruction-import",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_cd9015f39a60251bee1b01baf5189de7427e5810f130aed39770f17ae69cee44"
|
||||
"ev_13f3401a3aaddc3f6d087ad160fa928374e142cdb9861254359b46865b8c992e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.dotnet-framework-x86-pinvoke",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_af02c0218a01138bd1f0f5ca052f95d348dfacb540040ab72d6ddc9889e3a7e4"
|
||||
"ev_6f026216826ca06e080b77ff608f02634fa945fbd2e151b4cd261c1444f8bbb1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.ilspy-reconstruction-oracle",
|
||||
"status": "unsupported",
|
||||
"evidence_ids": [
|
||||
"ev_5df12074aeb26d67bc954fd9230076bd0c3ce9969600e19c59ba02090a9988f2"
|
||||
"ev_65a94c4de03f9c6ebdaff3f381cca377821f349d3ca6dfad47a31cc586fae939"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.malformed-metadata",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_5d6aadf199ae090e0ddebc10a51aab8a410fa778a6ae8454fedfb10d240cdac2"
|
||||
"ev_deae4259ea4395623176de40d06c2abcbc2b3a77f0bfb068d76e568d801f45a8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.managed-native-verification",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_9f1862276fe8869b86105eb91154603922eeb95fec33a10c354b2d6c9c3fe077"
|
||||
"ev_0edcbbcaa1191a4e5324a437f739957b77f9ac45e7897724337f2d6d06b3f9d1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.modern-dotnet-anycpu",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_c9aef74724cf555bcedce8a9dbf46327b2f92b95b03eb363f966885d429e6882"
|
||||
"ev_d1af50e8dc04cd589385769efd8758e82095425f6e9c1cc4bf2f530c6b434c93"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.mvid-and-token-drift",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_55808bf3ee5ec9d87cee751462077a6bce88701c4fcf66822ba1d7d33706706b"
|
||||
"ev_5493f40bc875535488d789a490361abcc1f76bfbdf52536d09ff127442a8e854"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.not-managed",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_a8166506ca91a4595b825f355836bb4becf35852c8547c55408c09698f4953a6"
|
||||
"ev_03ce0cc0183ee0710f8120e837565129559ab3bc41927df326066370c2341699"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.operator-local-manifest",
|
||||
"status": "unsupported",
|
||||
"evidence_ids": [
|
||||
"ev_03d1a151446834fbee7c093e82121b714a0cc1e6167ad84f47a79ffb9e2602ce"
|
||||
"ev_9d3e50da1d372c55f46649e69eae702cbe8f0ba6a81d1e46afc3e1a284c81757"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.operator-manifest-self-test",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_74d7f9c75a4476196664542d790e2532435f2a1c54349b481967e51e2ccc9453"
|
||||
"ev_f974f4b9d039cda841894c6173cae86427ecef48f4e7a4d6c73edf7b23af2b2a"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.runtime-correlation-admission-plan",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_5a80df0b92dc2280361e900ce590bc88ea52eba3dc8e216a0ce282f503e3cb05"
|
||||
"ev_3e546c20493faf9696502171a5cfe9504569cffb22f0561ee3000f0027b9bbd8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.unicode-obfuscated-identifiers",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_3b61c79ec152c70693278a70ad2ca6528ce0a9c9ab389d3ec9e4520de79082a7"
|
||||
"ev_c4f38ce76d8359d92ee8e9442503e47b7913550442349a0df688bd274a3def67"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.x64-ready-to-run-native-body",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_81f41b1b3809c2fe1964f7abbcbd2faeb14c723938639c2e0bb16cd58746aa1a"
|
||||
"ev_3e05ef1d6a796d3d4cb3372fe8e549efec759c59bf09d8ff660059a2e448cbc0"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"manifest_id": "ecm_ee8e5f2f4ea090052051d2994cbff148d9c796e1bc28b2fdc145aa5fa48cd248",
|
||||
"manifest_id": "ecm_fcfc6a2f890ccb4a5a7d19dce941b8f7644f05babd89e223100ef7b8b7a55afc",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"environment": {
|
||||
"platform": "linux",
|
||||
"architecture": "x64",
|
||||
"platform": "portable",
|
||||
"architecture": "portable",
|
||||
"runtime": "node",
|
||||
"runtime_version": "24"
|
||||
"runtime_version": "portable"
|
||||
},
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "6a910f067c946d545f5eb711eacbdf804839dbfb6b3b7b6ea91769be37ae47ce"
|
||||
"sha256": "6a884e8e2df17da0df0e8fda01aa3d66726fd7454c5606752c2b3ba3aa9e5449"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
@@ -30,8 +29,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.decompiler-reconstruction-import",
|
||||
@@ -45,8 +43,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.dotnet-framework-x86-pinvoke",
|
||||
@@ -60,8 +57,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.ilspy-reconstruction-oracle",
|
||||
@@ -73,8 +69,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.malformed-metadata",
|
||||
@@ -88,8 +83,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.managed-native-verification",
|
||||
@@ -104,8 +98,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.modern-dotnet-anycpu",
|
||||
@@ -119,8 +112,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.mvid-and-token-drift",
|
||||
@@ -135,8 +127,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.not-managed",
|
||||
@@ -150,8 +141,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.operator-local-manifest",
|
||||
@@ -163,8 +153,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.operator-manifest-self-test",
|
||||
@@ -178,8 +167,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.runtime-correlation-admission-plan",
|
||||
@@ -193,8 +181,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.unicode-obfuscated-identifiers",
|
||||
@@ -208,8 +195,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.x64-ready-to-run-native-body",
|
||||
@@ -223,8 +209,7 @@
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
},
|
||||
"result_schema_version": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ The P0 accepts exactly:
|
||||
|
||||
- a Windows x64 host;
|
||||
- Node.js 22.19+ or 24.11+;
|
||||
- an operator-installed official Ghidra 12.1.2 distribution;
|
||||
- an operator-installed official Ghidra 12.1.4 distribution;
|
||||
- a 64-bit full JDK 21;
|
||||
- an explicit native, non-managed, non-DLL x86-64 PE application; and
|
||||
- the existing 19 read-only Ghidra inventory and function-analysis operations.
|
||||
@@ -31,7 +31,7 @@ Install REA, Ghidra, and JDK 21 separately. In PowerShell:
|
||||
|
||||
```powershell
|
||||
npm install --global rea-agents
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.2_PUBLIC"
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
|
||||
$env:REA_ANALYSIS_PROVIDER = "ghidra"
|
||||
|
||||
@@ -58,7 +58,7 @@ three environment variables above. A representative configuration is:
|
||||
],
|
||||
"env": {
|
||||
"REA_ANALYSIS_PROVIDER": "ghidra",
|
||||
"GHIDRA_INSTALL_DIR": "C:\\tools\\ghidra_12.1.2_PUBLIC",
|
||||
"GHIDRA_INSTALL_DIR": "C:\\tools\\ghidra_12.1.4_PUBLIC",
|
||||
"JAVA_HOME": "C:\\Program Files\\Java\\jdk-21"
|
||||
}
|
||||
}
|
||||
@@ -95,8 +95,8 @@ and no verified no-follow equivalent for every path component.
|
||||
|
||||
Linux continues to use a mode-restricted Unix socket. Windows uses a Java
|
||||
listener bound to `127.0.0.1` on an ephemeral port. The bridge atomically
|
||||
publishes a bounded endpoint record containing only schema version, literal
|
||||
loopback host, and port. The random 256-bit request token remains only in the
|
||||
publishes a bounded endpoint record containing only the literal loopback host
|
||||
and port. The random 256-bit request token remains only in the
|
||||
ephemeral session descriptor and every request is authenticated.
|
||||
|
||||
`analyzeHeadless.bat` is invoked through an absolute `cmd.exe` with a fixed
|
||||
@@ -138,14 +138,14 @@ and compares the packaged MCP catalog with `TOOL_CONTRACTS`.
|
||||
The controlled real-engine lane is intentionally separate:
|
||||
|
||||
```powershell
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.2_PUBLIC"
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
|
||||
npm ci
|
||||
npm run verify:ghidra:windows
|
||||
```
|
||||
|
||||
The verifier creates an ignored deterministic x86-64 PE fixture, checks its
|
||||
fixed SHA-256, opens Ghidra 12.1.2 through the production Java bridge, exercises
|
||||
fixed SHA-256, opens Ghidra 12.1.4 through the production Java bridge, exercises
|
||||
all 19 admitted operations, verifies the target/snapshot/import digest chain,
|
||||
and checks endpoint, project, process, and runtime cleanup. The GitHub workflow
|
||||
accepts only the fixed `real-ghidra-windows` repository-dispatch event against
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
|
||||
npm install --global "${prefix_args[@]}" "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
|
||||
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
|
||||
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
|
||||
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version."
|
||||
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
|
||||
|
||||
printf 'REA %s is installed.\n' "$version"
|
||||
if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
{
|
||||
"$schema": "./node_modules/knip/schema.json",
|
||||
"entry": ["src/main.ts", "src/cli.ts"],
|
||||
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
"ignoreDependencies": [],
|
||||
"ignoreDependencies": ["electron"],
|
||||
"ignoreBinaries": ["ps"],
|
||||
"ignoreExportsUsedInFile": true,
|
||||
"ignoreIssues": {
|
||||
"src/domain/mobileApplicationInvestigation.ts": ["types"],
|
||||
"src/domain/objcSwiftMetadata.ts": ["types"],
|
||||
"src/domain/interfaceBuilderGraph.ts": ["types"],
|
||||
"src/domain/nativeInvestigationGraph.ts": ["types"]
|
||||
},
|
||||
"rules": {
|
||||
"files": "error",
|
||||
"dependencies": "error",
|
||||
|
||||
Generated
+76
-16
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"version": "2.5.1",
|
||||
"version": "3.2.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rea-agents",
|
||||
"version": "2.5.1",
|
||||
"version": "3.2.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "8.0.4",
|
||||
@@ -15,8 +15,8 @@
|
||||
"@electron/asar": "4.2.0",
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
"@modelcontextprotocol/client": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/client": "2.0.0",
|
||||
"@modelcontextprotocol/server": "2.0.0",
|
||||
"@xterm/addon-serialize": "0.14.0",
|
||||
"@xterm/headless": "6.0.0",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
@@ -47,6 +47,7 @@
|
||||
"@types/write-file-atomic": "4.0.3",
|
||||
"@types/ws": "8.18.1",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"ajv": "8.20.0",
|
||||
"husky": "9.1.7",
|
||||
"jscpd": "5.0.12",
|
||||
"knip": "6.26.0",
|
||||
@@ -56,7 +57,8 @@
|
||||
"turbo": "2.10.5",
|
||||
"typedoc": "0.28.20",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.1.10"
|
||||
"vitest": "4.1.10",
|
||||
"yaml": "2.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.19.0 || >=24.11.0"
|
||||
@@ -369,12 +371,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/client": {
|
||||
"version": "2.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-VNHA/UXDk7mCpVl+jOg5B4WMRRD2OEl+it360Lhi6HiCbrIB2f6pZ0cqSDKXQVUtZvqnhdQHzZAM9h8EKWhq/A==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz",
|
||||
"integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/core": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/core": "2.0.0",
|
||||
"cross-spawn": "^7.0.5",
|
||||
"eventsource": "^3.0.2",
|
||||
"eventsource-parser": "^3.0.0",
|
||||
@@ -387,9 +389,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/core": {
|
||||
"version": "2.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
|
||||
"integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zod": "^4.2.0"
|
||||
@@ -399,12 +401,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/server": {
|
||||
"version": "2.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-pjMZcNEt1dOq0aJCcY3b7w1Ayh+qmQN2xlfTELcGV9yiAjEGIczBPBLWWW0k0zzo5T8kiv15jtKPsWeHGxLvLg==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
|
||||
"integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/core": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/core": "2.0.0",
|
||||
"zod": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2157,6 +2159,23 @@
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
|
||||
@@ -2711,6 +2730,13 @@
|
||||
"node": ">=12.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-string-truncated-width": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
|
||||
@@ -2726,6 +2752,23 @@
|
||||
"fast-string-truncated-width": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
|
||||
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-wrap-ansi": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
|
||||
@@ -3375,6 +3418,13 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/knip": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-6.26.0.tgz",
|
||||
@@ -4474,6 +4524,16 @@
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
|
||||
+48
-18
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"version": "2.5.1",
|
||||
"mcpName": "io.github.morluto/rea",
|
||||
"version": "3.2.1",
|
||||
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -41,8 +42,11 @@
|
||||
"dist",
|
||||
"install.sh",
|
||||
"bridge/hopper_bridge.py",
|
||||
"bridge/native/ReaNativeUI.swift",
|
||||
"bridge/ghidra/ReaGhidraBridge.java",
|
||||
"scripts/rea.mjs",
|
||||
"scripts/electron-active-hook.cjs",
|
||||
"scripts/electron-active-hook-boundaries.cjs",
|
||||
"scripts/hopper-demo-x11.py",
|
||||
"scripts/prepack.mjs",
|
||||
"scripts/prepare.mjs",
|
||||
@@ -54,16 +58,21 @@
|
||||
"deps:check": "node scripts/check-dependency-install.mjs",
|
||||
"metadata:generate": "node scripts/generate-package-metadata.mjs",
|
||||
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
|
||||
"prebuild": "npm run deps:check && npm run metadata:check",
|
||||
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
|
||||
"mcp-catalog:generate": "npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs",
|
||||
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
||||
"prebuild": "npm run deps:check && npm run metadata:check && node scripts/clean-build-output.mjs",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"precheck": "npm run deps:check",
|
||||
"check": "turbo run typecheck lint format:check knip metadata:check",
|
||||
"precheck:fast": "npm run deps:check",
|
||||
"check:fast": "turbo run typecheck lint",
|
||||
"precheck:changed": "npm run deps:check",
|
||||
"check:changed": "turbo run typecheck lint && npm run test:changed",
|
||||
"precheck:test": "npm run deps:check",
|
||||
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
|
||||
"precheck:pr": "npm run deps:check",
|
||||
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run docs:check:built",
|
||||
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check",
|
||||
"precheck:ci": "npm run deps:check",
|
||||
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
|
||||
"build:cached": "turbo run build",
|
||||
@@ -76,32 +85,48 @@
|
||||
"jscpd": "jscpd src/ tests/",
|
||||
"scan:todos": "node scripts/scan-todos.mjs",
|
||||
"start": "node scripts/rea.mjs mcp",
|
||||
"pretest": "npm run build:cached",
|
||||
"test": "vitest run",
|
||||
"test:run": "vitest run",
|
||||
"test": "node scripts/run-exclusive.mjs test npm run test:with-build",
|
||||
"test:with-build": "npm run build:cached && vitest run",
|
||||
"test:run": "node scripts/run-exclusive.mjs test vitest run",
|
||||
"test:local": "node scripts/run-exclusive.mjs test vitest run --changed --passWithNoTests --maxWorkers=1",
|
||||
"pretest:fast": "npm run build:cached",
|
||||
"test:fast": "vitest run --project parallel --project subprocess",
|
||||
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance",
|
||||
"pretest:boundary": "npm run build:cached",
|
||||
"test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global",
|
||||
"pretest:mcp": "npm run build:cached",
|
||||
"test:mcp": "vitest run --project mcp-boundary",
|
||||
"pretest:acceptance": "npm run build:cached",
|
||||
"test:acceptance": "vitest run --project acceptance",
|
||||
"pretest:changed": "npm run build:cached",
|
||||
"test:changed": "vitest run --project parallel --project subprocess --changed",
|
||||
"test:changed": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
|
||||
"pretest:integration": "npm run build:cached",
|
||||
"test:integration": "vitest run --project serial-integration",
|
||||
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
|
||||
"pretest:coverage": "npm run build:cached",
|
||||
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
|
||||
"test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
|
||||
"pretest:ci:shard": "npm run build:cached",
|
||||
"test:ci:shard": "npm run test:ci:shard:run",
|
||||
"test:ci:shard:run": "vitest run --coverage.enabled --coverage.provider=v8 --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 --reporter=default --reporter=blob",
|
||||
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8 --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
|
||||
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8",
|
||||
"pretest:verbose": "npm run build:cached",
|
||||
"test:verbose": "vitest run --reporter=verbose",
|
||||
"pretest:watch": "npm run build:cached",
|
||||
"test:watch": "vitest --project parallel --project subprocess --changed",
|
||||
"test:watch": "vitest --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
|
||||
"pretest:watch:all": "npm run build:cached",
|
||||
"test:watch:all": "vitest --changed",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
|
||||
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
|
||||
"verify:apple-dispatch": "npm run build:cached && node scripts/verify-apple-dispatch.mjs",
|
||||
"verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs",
|
||||
"verify:keyed-archive": "npm run build:cached && node scripts/verify-keyed-archive.mjs",
|
||||
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
|
||||
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
|
||||
"verify:interface-builder": "npm run build:cached && node scripts/verify-interface-builder.mjs",
|
||||
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
|
||||
"verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
|
||||
"verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs",
|
||||
"verify:electron": "npm run build:cached && node scripts/verify-real-electron.mjs",
|
||||
"verify:inspector": "npm run build:cached && node scripts/verify-real-javascript-runtime-observation.mjs",
|
||||
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.mjs",
|
||||
"verify:application-workflows": "npm run build:cached && node scripts/verify-local-application-workflows.mjs",
|
||||
@@ -123,12 +148,15 @@
|
||||
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
|
||||
"docs:api": "typedoc",
|
||||
"docs:api:cached": "turbo run docs:api",
|
||||
"docs:generate": "npm run metadata:generate && npm run build:cached && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
|
||||
"docs:check": "turbo run docs:check:built",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
|
||||
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
|
||||
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
|
||||
"docs:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
|
||||
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
|
||||
"prepack": "node scripts/prepack.mjs",
|
||||
"prepare": "node scripts/prepare.mjs"
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
"verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs",
|
||||
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
||||
@@ -144,8 +172,8 @@
|
||||
"@electron/asar": "4.2.0",
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
"@modelcontextprotocol/client": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/client": "2.0.0",
|
||||
"@modelcontextprotocol/server": "2.0.0",
|
||||
"@xterm/addon-serialize": "0.14.0",
|
||||
"@xterm/headless": "6.0.0",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
@@ -172,6 +200,7 @@
|
||||
"@types/write-file-atomic": "4.0.3",
|
||||
"@types/ws": "8.18.1",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"ajv": "8.20.0",
|
||||
"husky": "9.1.7",
|
||||
"jscpd": "5.0.12",
|
||||
"knip": "6.26.0",
|
||||
@@ -181,6 +210,7 @@
|
||||
"turbo": "2.10.5",
|
||||
"typedoc": "0.28.20",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.1.10"
|
||||
"vitest": "4.1.10",
|
||||
"yaml": "2.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,16 @@
|
||||
"type": "generic",
|
||||
"path": "src/generatedPackageMetadata.ts"
|
||||
},
|
||||
{
|
||||
"type": "json",
|
||||
"path": "server.json",
|
||||
"jsonpath": "$.version"
|
||||
},
|
||||
{
|
||||
"type": "json",
|
||||
"path": "server.json",
|
||||
"jsonpath": "$.packages[0].version"
|
||||
},
|
||||
{
|
||||
"type": "json",
|
||||
"path": "docs/product-catalog.json",
|
||||
|
||||
@@ -37,7 +37,6 @@ await buildPortableFixtures();
|
||||
if (hostPlatform === "darwin") await buildDarwinFixtures();
|
||||
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
platform: hostPlatform,
|
||||
architecture,
|
||||
generatedAt: new Date(0).toISOString(),
|
||||
@@ -103,9 +102,9 @@ async function buildPortableFixtures() {
|
||||
join(outputRoot, "large"),
|
||||
],
|
||||
expectations: {
|
||||
symbolPrefix: `${symbolPrefix}rea_page_`,
|
||||
symbolPrefix: `${symbolPrefix}rea_inventory_`,
|
||||
symbolCount: LARGE_FIXTURE_COUNT,
|
||||
stringPrefix: "REA_PAGE_",
|
||||
stringPrefix: "REA_INVENTORY_",
|
||||
stringCount: LARGE_FIXTURE_COUNT,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
await Promise.all([
|
||||
rm(join(root, "dist"), { recursive: true, force: true }),
|
||||
rm(join(root, ".cache", "tsconfig.build.tsbuildinfo"), { force: true }),
|
||||
]);
|
||||
@@ -0,0 +1,394 @@
|
||||
"use strict";
|
||||
|
||||
// oxlint-disable-next-line max-lines-per-function -- all adapters must close over one bounded capture context.
|
||||
const createElectronActiveBoundaryPatches = ({
|
||||
identity,
|
||||
patchEmitter,
|
||||
processType,
|
||||
recordIpc,
|
||||
recordRuntime,
|
||||
shape,
|
||||
}) => {
|
||||
const patchedSessions = new WeakSet();
|
||||
const patchIpcRenderer = (ipcRenderer) => {
|
||||
if (ipcRenderer === null || ipcRenderer === undefined) return;
|
||||
for (const [method, kind] of [
|
||||
["send", "ipc-renderer-send"],
|
||||
["sendSync", "ipc-renderer-send"],
|
||||
["invoke", "ipc-renderer-invoke"],
|
||||
["postMessage", "ipc-renderer-post-message"],
|
||||
]) {
|
||||
const original = ipcRenderer[method];
|
||||
if (typeof original !== "function") continue;
|
||||
ipcRenderer[method] = function patchedRendererIpc(channel, ...args) {
|
||||
const recorded = recordIpc(kind, channel, args, {
|
||||
direction: "renderer-to-main",
|
||||
sender: processType(),
|
||||
receiver: "main",
|
||||
frame: null,
|
||||
process_type: processType(),
|
||||
error: false,
|
||||
});
|
||||
try {
|
||||
const result = original.call(this, channel, ...args);
|
||||
if (result !== null && typeof result?.then === "function")
|
||||
return result.then(
|
||||
(value) => {
|
||||
if (recorded !== null) recorded.result_shape = shape(value);
|
||||
return value;
|
||||
},
|
||||
(cause) => {
|
||||
if (recorded !== null) recorded.error = true;
|
||||
throw cause;
|
||||
},
|
||||
);
|
||||
if (recorded !== null) recorded.result_shape = shape(result);
|
||||
return result;
|
||||
} catch (cause) {
|
||||
if (recorded !== null) recorded.error = true;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const patchContextBridge = (contextBridge) => {
|
||||
if (contextBridge === null || contextBridge === undefined) return;
|
||||
for (const method of ["exposeInMainWorld", "exposeInIsolatedWorld"]) {
|
||||
const original = contextBridge[method];
|
||||
if (typeof original !== "function") continue;
|
||||
contextBridge[method] = function patchedContextBridge(...args) {
|
||||
const target = typeof args[0] === "string" ? args[0] : null;
|
||||
recordRuntime("preload", "context-bridge-exposure", "attempted", {
|
||||
target,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
process_type: processType(),
|
||||
});
|
||||
try {
|
||||
const result = original.apply(this, args);
|
||||
recordRuntime("preload", "context-bridge-exposure", "completed", {
|
||||
target,
|
||||
process_type: processType(),
|
||||
});
|
||||
return result;
|
||||
} catch (cause) {
|
||||
recordRuntime("preload", "context-bridge-exposure", "failed", {
|
||||
target,
|
||||
error: true,
|
||||
process_type: processType(),
|
||||
});
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const patchSession = (session) => {
|
||||
if (session === null || session === undefined) return;
|
||||
if (
|
||||
(typeof session !== "object" && typeof session !== "function") ||
|
||||
patchedSessions.has(session)
|
||||
)
|
||||
return;
|
||||
patchedSessions.add(session);
|
||||
patchEmitter(session, (event, args) => {
|
||||
const kind =
|
||||
event === "will-download"
|
||||
? "download"
|
||||
: event.includes("certificate") || event.includes("permission")
|
||||
? "permission"
|
||||
: event.includes("window") || event.includes("popup")
|
||||
? "popup-attempt"
|
||||
: "web-contents-lifecycle";
|
||||
if (event === "will-download") {
|
||||
const cancel = args[1]?.cancel;
|
||||
if (typeof cancel === "function") cancel.call(args[1]);
|
||||
}
|
||||
recordRuntime(
|
||||
kind,
|
||||
event,
|
||||
event === "will-download" ? "blocked" : "observed",
|
||||
{
|
||||
target: identity(args[0], "webContents"),
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
error: event === "will-download",
|
||||
},
|
||||
);
|
||||
});
|
||||
const originalPermissionRequest = session.setPermissionRequestHandler;
|
||||
if (typeof originalPermissionRequest === "function")
|
||||
session.setPermissionRequestHandler =
|
||||
function blockedPermissionRequest() {
|
||||
return originalPermissionRequest.call(
|
||||
this,
|
||||
(webContents, permission, callback) => {
|
||||
recordRuntime("permission", "permission-request", "blocked", {
|
||||
target: identity(webContents, "webContents"),
|
||||
argument_shapes: [shape(permission)],
|
||||
error: true,
|
||||
});
|
||||
if (typeof callback === "function") callback(false);
|
||||
},
|
||||
);
|
||||
};
|
||||
const originalPermissionCheck = session.setPermissionCheckHandler;
|
||||
if (typeof originalPermissionCheck === "function")
|
||||
session.setPermissionCheckHandler = function blockedPermissionCheck() {
|
||||
return originalPermissionCheck.call(this, (webContents, permission) => {
|
||||
recordRuntime("permission", "permission-check", "blocked", {
|
||||
target: identity(webContents, "webContents"),
|
||||
argument_shapes: [shape(permission)],
|
||||
error: true,
|
||||
});
|
||||
return false;
|
||||
});
|
||||
};
|
||||
const originalCertificate = session.setCertificateVerifyProc;
|
||||
if (typeof originalCertificate === "function")
|
||||
session.setCertificateVerifyProc = function blockedCertificate() {
|
||||
return originalCertificate.call(this, (request, callback) => {
|
||||
recordRuntime("permission", "certificate-error", "blocked", {
|
||||
argument_shapes: [shape(request)],
|
||||
error: true,
|
||||
});
|
||||
if (typeof callback === "function") callback(-2);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const patchWindowOpenHandler = (webContents, contentsId) => {
|
||||
if (webContents === null || webContents === undefined) return;
|
||||
const original = webContents.setWindowOpenHandler;
|
||||
if (typeof original !== "function") return;
|
||||
const createGuardedHandler =
|
||||
(handler) =>
|
||||
(...args) => {
|
||||
try {
|
||||
if (typeof handler === "function") handler(...args);
|
||||
} catch {
|
||||
recordRuntime("popup-attempt", "window-open-handler", "failed", {
|
||||
target: contentsId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
error: true,
|
||||
});
|
||||
return { action: "deny" };
|
||||
}
|
||||
recordRuntime("popup-attempt", "window-open-handler", "blocked", {
|
||||
target: contentsId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
error: true,
|
||||
});
|
||||
return { action: "deny" };
|
||||
};
|
||||
try {
|
||||
original.call(webContents, createGuardedHandler(undefined));
|
||||
} catch {
|
||||
recordRuntime("popup-attempt", "window-open-handler-install", "failed", {
|
||||
target: contentsId,
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
webContents.setWindowOpenHandler = function blockedWindowOpenHandler(
|
||||
handler,
|
||||
) {
|
||||
return original.call(this, createGuardedHandler(handler));
|
||||
};
|
||||
};
|
||||
|
||||
const patchSessionManager = (sessionModule) => {
|
||||
if (sessionModule === null || sessionModule === undefined) return;
|
||||
patchSession(sessionModule.defaultSession);
|
||||
for (const method of ["fromPartition", "fromPath"]) {
|
||||
const original = sessionModule[method];
|
||||
if (typeof original !== "function") continue;
|
||||
sessionModule[method] = function patchedSessionFactory(...args) {
|
||||
const session = original.apply(this, args);
|
||||
patchSession(session);
|
||||
return session;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const patchApplicationEffects = (app) => {
|
||||
if (app === null || app === undefined) return;
|
||||
for (const method of [
|
||||
"relaunch",
|
||||
"setAsDefaultProtocolClient",
|
||||
"setLoginItemSettings",
|
||||
]) {
|
||||
const original = app[method];
|
||||
if (typeof original !== "function") continue;
|
||||
app[method] = function blockedApplicationEffect(...args) {
|
||||
recordRuntime(
|
||||
method === "relaunch" ? "updater" : "protocol",
|
||||
method,
|
||||
"blocked",
|
||||
{ argument_shapes: args.map((value) => shape(value)), error: true },
|
||||
);
|
||||
return method === "setAsDefaultProtocolClient" ? false : undefined;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const patchNativeAddonLoading = () => {
|
||||
const original = process.dlopen;
|
||||
if (typeof original !== "function") return;
|
||||
process.dlopen = function patchedDlopen(module, filename, ...args) {
|
||||
let digest = null;
|
||||
try {
|
||||
const fs = require("node:fs");
|
||||
const crypto = require("node:crypto");
|
||||
const stat = fs.statSync(filename);
|
||||
if (stat.isFile() && stat.size <= 16 * 1024 * 1024)
|
||||
digest = crypto
|
||||
.createHash("sha256")
|
||||
.update(fs.readFileSync(filename))
|
||||
.digest("hex");
|
||||
} catch {
|
||||
digest = null;
|
||||
}
|
||||
recordRuntime("native-addon", "dlopen", "attempted", {
|
||||
artifact_path: typeof filename === "string" ? filename : null,
|
||||
artifact_sha256: digest,
|
||||
argument_shapes: [
|
||||
shape(module),
|
||||
shape(filename),
|
||||
...args.map((value) => shape(value)),
|
||||
],
|
||||
});
|
||||
try {
|
||||
const result = original.call(this, module, filename, ...args);
|
||||
recordRuntime("native-addon", "dlopen", "completed", {
|
||||
artifact_path: typeof filename === "string" ? filename : null,
|
||||
artifact_sha256: digest,
|
||||
});
|
||||
return result;
|
||||
} catch (cause) {
|
||||
recordRuntime("native-addon", "dlopen", "failed", {
|
||||
artifact_path: typeof filename === "string" ? filename : null,
|
||||
artifact_sha256: digest,
|
||||
error: true,
|
||||
});
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const patchUtilityProcess = (utilityProcess) => {
|
||||
if (utilityProcess === null || utilityProcess === undefined) return;
|
||||
const original = utilityProcess.fork;
|
||||
if (typeof original !== "function") return;
|
||||
utilityProcess.fork = function patchedFork(...args) {
|
||||
recordRuntime("process-lifecycle", "utility-process-fork", "attempted", {
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
process_type: "main",
|
||||
});
|
||||
const child = original.apply(this, args);
|
||||
const childId = identity(child, "utility");
|
||||
recordRuntime("process-lifecycle", "utility-process-fork", "completed", {
|
||||
target: childId,
|
||||
process_type: "main",
|
||||
});
|
||||
patchEmitter(child, (event, eventArgs) => {
|
||||
if (event === "message") {
|
||||
recordIpc("ipc-utility-to-main", null, eventArgs, {
|
||||
direction: "utility-to-main",
|
||||
sender: childId,
|
||||
receiver: "main",
|
||||
target: childId,
|
||||
process_type: "main",
|
||||
error: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
recordRuntime("process-lifecycle", `utility.${event}`, "observed", {
|
||||
target: childId,
|
||||
process_type: "utility",
|
||||
argument_shapes: eventArgs.map((value) => shape(value)),
|
||||
});
|
||||
});
|
||||
if (child && typeof child.postMessage === "function") {
|
||||
const postMessage = child.postMessage.bind(child);
|
||||
child.postMessage = (...messageArgs) => {
|
||||
const recorded = recordIpc(
|
||||
"utility-process-message",
|
||||
null,
|
||||
messageArgs,
|
||||
{
|
||||
direction: "main-to-utility",
|
||||
sender: "main",
|
||||
receiver: childId,
|
||||
target: childId,
|
||||
process_type: "main",
|
||||
error: false,
|
||||
},
|
||||
);
|
||||
try {
|
||||
const result = postMessage(...messageArgs);
|
||||
if (recorded !== null) recorded.result_shape = shape(result);
|
||||
return result;
|
||||
} catch (cause) {
|
||||
if (recorded !== null) recorded.error = true;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
}
|
||||
return child;
|
||||
};
|
||||
};
|
||||
|
||||
const patchShell = (shell) => {
|
||||
if (shell === null || shell === undefined) return;
|
||||
for (const method of ["openExternal", "openPath"]) {
|
||||
const original = shell[method];
|
||||
if (typeof original !== "function") continue;
|
||||
shell[method] = function blockedShellCall(...args) {
|
||||
recordRuntime("shell-attempt", method, "blocked", {
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
error: true,
|
||||
});
|
||||
return Promise.reject(new Error("External shell effects are blocked"));
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const patchChildProcess = () => {
|
||||
const childProcess = require("node:child_process");
|
||||
for (const method of ["spawn", "fork", "exec", "execFile"]) {
|
||||
const original = childProcess[method];
|
||||
if (typeof original !== "function") continue;
|
||||
childProcess[method] = function patchedChildProcess(...args) {
|
||||
recordRuntime("process-lifecycle", `child.${method}`, "attempted", {
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
});
|
||||
const child = original.apply(this, args);
|
||||
const childId = identity(child, "child");
|
||||
recordRuntime("process-lifecycle", `child.${method}`, "completed", {
|
||||
target: childId,
|
||||
});
|
||||
patchEmitter(child, (event, eventArgs) =>
|
||||
recordRuntime("process-lifecycle", `child.${event}`, "observed", {
|
||||
target: childId,
|
||||
argument_shapes: eventArgs.map((value) => shape(value)),
|
||||
}),
|
||||
);
|
||||
return child;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
patchApplicationEffects,
|
||||
patchChildProcess,
|
||||
patchContextBridge,
|
||||
patchIpcRenderer,
|
||||
patchNativeAddonLoading,
|
||||
patchSession,
|
||||
patchSessionManager,
|
||||
patchShell,
|
||||
patchWindowOpenHandler,
|
||||
patchUtilityProcess,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { createElectronActiveBoundaryPatches };
|
||||
@@ -0,0 +1,469 @@
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
createElectronActiveBoundaryPatches,
|
||||
} = require("./electron-active-hook-boundaries.cjs");
|
||||
|
||||
const ipcEventKinds = new Set([
|
||||
"main-handler-invocation",
|
||||
"main-event-invocation",
|
||||
"utility-process-fork",
|
||||
"utility-process-message",
|
||||
"ipc-main-to-renderer",
|
||||
"ipc-utility-to-main",
|
||||
"ipc-renderer-send",
|
||||
"ipc-renderer-invoke",
|
||||
"ipc-renderer-post-message",
|
||||
]);
|
||||
const events = [];
|
||||
let observed = 0;
|
||||
let observedIpc = 0;
|
||||
let observedRuntime = 0;
|
||||
let sequence = 0;
|
||||
let correlationSequence = 0;
|
||||
|
||||
const shape = (value, depth = 0) => {
|
||||
if (depth > 2 || value === null) return value === null ? "null" : "nested";
|
||||
if (Array.isArray(value)) return "array";
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
return "string";
|
||||
case "number":
|
||||
return "number";
|
||||
case "boolean":
|
||||
return "boolean";
|
||||
case "undefined":
|
||||
return "undefined";
|
||||
case "function":
|
||||
return "function";
|
||||
case "object":
|
||||
return "object";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const isAllowedNavigation = (value) => {
|
||||
if (typeof value !== "string") return true;
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (["file:", "data:", "about:"].includes(parsed.protocol)) return true;
|
||||
return (
|
||||
["http:", "https:"].includes(parsed.protocol) &&
|
||||
["127.0.0.1", "localhost", "[::1]", "::1"].includes(parsed.hostname)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const processType = () =>
|
||||
typeof process.type === "string" && process.type.length > 0
|
||||
? process.type
|
||||
: "main";
|
||||
|
||||
const identity = (value, prefix) => {
|
||||
if (value === null || value === undefined) return null;
|
||||
const id = value.id;
|
||||
const processId = value.pid;
|
||||
return typeof id === "string" || typeof id === "number"
|
||||
? `${prefix}:${String(id)}`
|
||||
: typeof processId === "number"
|
||||
? `${prefix}:pid:${String(processId)}`
|
||||
: null;
|
||||
};
|
||||
|
||||
const frameIdentity = (event) => {
|
||||
const frame = event?.senderFrame ?? event?.frame;
|
||||
const id = frame?.frameTreeNodeId ?? frame?.routingId ?? frame?.id;
|
||||
return typeof id === "string" || typeof id === "number"
|
||||
? `frame:${String(id)}`
|
||||
: null;
|
||||
};
|
||||
|
||||
const record = (event) => {
|
||||
observed += 1;
|
||||
if (ipcEventKinds.has(event.kind)) observedIpc += 1;
|
||||
else observedRuntime += 1;
|
||||
const raw = {
|
||||
sequence: ++sequence,
|
||||
correlation_id:
|
||||
typeof event.correlation_id === "string"
|
||||
? event.correlation_id
|
||||
: `capture:${process.pid}:${++correlationSequence}`,
|
||||
event: null,
|
||||
phase: "observed",
|
||||
channel: null,
|
||||
direction: null,
|
||||
sender: null,
|
||||
receiver: null,
|
||||
frame: null,
|
||||
target: null,
|
||||
argument_shapes: [],
|
||||
result_shape: null,
|
||||
process_type: processType(),
|
||||
source: "electron-active-hook",
|
||||
capture_method: "api-wrapper",
|
||||
artifact_path: null,
|
||||
artifact_sha256: null,
|
||||
error: false,
|
||||
...event,
|
||||
};
|
||||
const argumentShapes = Array.isArray(raw.argument_shapes)
|
||||
? raw.argument_shapes
|
||||
: [];
|
||||
events.push({
|
||||
...raw,
|
||||
correlation_id: raw.correlation_id,
|
||||
event: typeof raw.event === "string" ? raw.event : null,
|
||||
channel: typeof raw.channel === "string" ? raw.channel : null,
|
||||
sender: typeof raw.sender === "string" ? raw.sender : null,
|
||||
receiver: typeof raw.receiver === "string" ? raw.receiver : null,
|
||||
frame: typeof raw.frame === "string" ? raw.frame : null,
|
||||
target: typeof raw.target === "string" ? raw.target : null,
|
||||
argument_shapes: argumentShapes.map((value) =>
|
||||
typeof value === "string" ? value : "unknown",
|
||||
),
|
||||
result_shape:
|
||||
typeof raw.result_shape === "string" ? raw.result_shape : null,
|
||||
process_type:
|
||||
typeof raw.process_type === "string" ? raw.process_type : null,
|
||||
artifact_path:
|
||||
typeof raw.artifact_path === "string" ? raw.artifact_path : null,
|
||||
artifact_sha256:
|
||||
typeof raw.artifact_sha256 === "string" ? raw.artifact_sha256 : null,
|
||||
});
|
||||
return events.at(-1);
|
||||
};
|
||||
|
||||
const recordRuntime = (kind, event, phase, details = {}) =>
|
||||
record({ kind, event, phase, ...details });
|
||||
|
||||
const recordIpc = (kind, channel, args, details = {}) =>
|
||||
(() => {
|
||||
const values = Array.isArray(args) ? args : [];
|
||||
return record({
|
||||
kind,
|
||||
channel: typeof channel === "string" ? channel : null,
|
||||
argument_shapes: values.map((value) => shape(value)),
|
||||
...details,
|
||||
});
|
||||
})();
|
||||
|
||||
const patchEmitter = (emitter, observeEvent) => {
|
||||
if (emitter === null || emitter === undefined) return;
|
||||
const original = emitter.emit;
|
||||
if (typeof original !== "function") return;
|
||||
if (emitter.__reaElectronActiveEmitPatched === true) return;
|
||||
try {
|
||||
emitter.__reaElectronActiveEmitPatched = true;
|
||||
emitter.emit = function patchedEmit(event, ...args) {
|
||||
if (typeof event === "string") observeEvent.call(this, event, args);
|
||||
return original.call(this, event, ...args);
|
||||
};
|
||||
} catch {
|
||||
globalThis.__reaElectronActiveHookError = true;
|
||||
}
|
||||
};
|
||||
|
||||
const recordInvocation = (kind, channel, event, args) =>
|
||||
recordIpc(kind, channel, args, {
|
||||
direction: "renderer-to-main",
|
||||
sender: identity(event?.sender, "webContents"),
|
||||
receiver: "main",
|
||||
frame: frameIdentity(event),
|
||||
process_type: "main",
|
||||
error: false,
|
||||
});
|
||||
|
||||
const patchIpcMain = (ipcMain) => {
|
||||
if (ipcMain === null || ipcMain === undefined) return;
|
||||
for (const [method, kind] of [
|
||||
["handle", "main-handler-invocation"],
|
||||
["on", "main-event-invocation"],
|
||||
["once", "main-event-invocation"],
|
||||
]) {
|
||||
const original = ipcMain[method];
|
||||
if (typeof original !== "function") continue;
|
||||
ipcMain[method] = function patchedIpcMain(channel, listener) {
|
||||
if (typeof listener !== "function")
|
||||
return original.call(this, channel, listener);
|
||||
const wrapped =
|
||||
kind === "main-handler-invocation"
|
||||
? async function wrappedHandler(event, ...args) {
|
||||
const recorded = recordInvocation(kind, channel, event, args);
|
||||
try {
|
||||
const result = await listener.call(this, event, ...args);
|
||||
if (recorded !== null) recorded.result_shape = shape(result);
|
||||
return result;
|
||||
} catch (cause) {
|
||||
if (recorded !== null) recorded.error = true;
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
: function wrappedEventListener(event, ...args) {
|
||||
const recorded = recordInvocation(kind, channel, event, args);
|
||||
try {
|
||||
return listener.call(this, event, ...args);
|
||||
} catch (cause) {
|
||||
if (recorded !== null) recorded.error = true;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
return original.call(this, channel, wrapped);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const patchNavigationMethods = (webContents, contentsId) => {
|
||||
for (const method of ["loadURL", "loadFile", "loadDataURL"]) {
|
||||
const original = webContents[method];
|
||||
if (typeof original !== "function") continue;
|
||||
webContents[method] = function patchedNavigation(...args) {
|
||||
recordRuntime("navigation", method, "attempted", {
|
||||
target: contentsId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
});
|
||||
if (method === "loadURL" && !isAllowedNavigation(args[0])) {
|
||||
recordRuntime("navigation", method, "blocked", {
|
||||
target: contentsId,
|
||||
error: true,
|
||||
});
|
||||
return Promise.reject(new Error("External navigation is blocked"));
|
||||
}
|
||||
try {
|
||||
const result = original.apply(this, args);
|
||||
if (result !== null && typeof result?.then === "function")
|
||||
return result.then(
|
||||
(value) => {
|
||||
recordRuntime("navigation", method, "completed", {
|
||||
target: contentsId,
|
||||
});
|
||||
return value;
|
||||
},
|
||||
(cause) => {
|
||||
recordRuntime("navigation", method, "failed", {
|
||||
target: contentsId,
|
||||
error: true,
|
||||
});
|
||||
throw cause;
|
||||
},
|
||||
);
|
||||
recordRuntime("navigation", method, "completed", {
|
||||
target: contentsId,
|
||||
});
|
||||
return result;
|
||||
} catch (cause) {
|
||||
recordRuntime("navigation", method, "failed", {
|
||||
target: contentsId,
|
||||
error: true,
|
||||
});
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const patchWebContentsIpc = (webContents, contentsId) => {
|
||||
const originalSend = webContents.send;
|
||||
if (typeof originalSend === "function")
|
||||
webContents.send = function patchedSend(channel, ...args) {
|
||||
const recorded = recordIpc("ipc-main-to-renderer", channel, args, {
|
||||
direction: "main-to-renderer",
|
||||
sender: "main",
|
||||
receiver: contentsId,
|
||||
target: contentsId,
|
||||
process_type: "main",
|
||||
error: false,
|
||||
});
|
||||
try {
|
||||
const result = originalSend.call(this, channel, ...args);
|
||||
if (recorded !== null) recorded.result_shape = shape(result);
|
||||
return result;
|
||||
} catch (cause) {
|
||||
if (recorded !== null) recorded.error = true;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
const originalPostMessage = webContents.postMessage;
|
||||
if (typeof originalPostMessage === "function")
|
||||
webContents.postMessage = function patchedPostMessage(
|
||||
channel,
|
||||
message,
|
||||
transfer,
|
||||
) {
|
||||
const recorded = recordIpc(
|
||||
"ipc-main-to-renderer",
|
||||
channel,
|
||||
[message, transfer],
|
||||
{
|
||||
direction: "main-to-renderer",
|
||||
sender: "main",
|
||||
receiver: contentsId,
|
||||
target: contentsId,
|
||||
process_type: "main",
|
||||
error: false,
|
||||
},
|
||||
);
|
||||
try {
|
||||
const result = originalPostMessage.call(
|
||||
this,
|
||||
channel,
|
||||
message,
|
||||
transfer,
|
||||
);
|
||||
if (recorded !== null) recorded.result_shape = shape(result);
|
||||
return result;
|
||||
} catch (cause) {
|
||||
if (recorded !== null) recorded.error = true;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const patchWebContents = (webContents, windowId) => {
|
||||
if (webContents === null || webContents === undefined) return;
|
||||
const contentsId = identity(webContents, "webContents");
|
||||
patchEmitter(webContents, (event, args) => {
|
||||
const navigation = [
|
||||
"did-start-loading",
|
||||
"did-stop-loading",
|
||||
"did-finish-load",
|
||||
"did-fail-load",
|
||||
"did-frame-finish-load",
|
||||
"will-navigate",
|
||||
"will-redirect",
|
||||
].includes(event);
|
||||
const kind = navigation
|
||||
? "navigation"
|
||||
: [
|
||||
"console-message",
|
||||
"render-process-gone",
|
||||
"crashed",
|
||||
"unresponsive",
|
||||
"responsive",
|
||||
].includes(event)
|
||||
? "error"
|
||||
: event === "did-create-window"
|
||||
? "popup-attempt"
|
||||
: "web-contents-lifecycle";
|
||||
if (event === "will-navigate" || event === "will-redirect") {
|
||||
if (!isAllowedNavigation(args[1])) {
|
||||
const preventDefault = args[0]?.preventDefault;
|
||||
if (typeof preventDefault === "function") preventDefault.call(args[0]);
|
||||
recordRuntime("navigation", event, "blocked", {
|
||||
target: contentsId,
|
||||
sender: windowId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
error: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
recordRuntime(kind, event, "observed", {
|
||||
target: contentsId,
|
||||
sender: windowId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
error: kind === "error",
|
||||
});
|
||||
if (event === "did-create-window") {
|
||||
const childWindow = args[0];
|
||||
patchWebContents(
|
||||
childWindow?.webContents,
|
||||
identity(childWindow, "window"),
|
||||
);
|
||||
}
|
||||
});
|
||||
patchNavigationMethods(webContents, contentsId);
|
||||
patchWebContentsIpc(webContents, contentsId);
|
||||
boundaries.patchWindowOpenHandler(webContents, contentsId);
|
||||
};
|
||||
|
||||
const patchBrowserWindow = (electron) => {
|
||||
const original = electron.BrowserWindow;
|
||||
if (typeof original !== "function") return;
|
||||
electron.BrowserWindow = new Proxy(original, {
|
||||
construct(target, args, newTarget) {
|
||||
const window = Reflect.construct(target, args, newTarget);
|
||||
const targetId = identity(window, "window");
|
||||
recordRuntime("window-lifecycle", "created", "completed", {
|
||||
target: targetId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
});
|
||||
const preload = args[0]?.webPreferences?.preload;
|
||||
if (typeof preload === "string")
|
||||
recordRuntime("preload", "configured", "completed", {
|
||||
target: targetId,
|
||||
artifact_path: preload,
|
||||
});
|
||||
patchEmitter(window, (event) =>
|
||||
recordRuntime("window-lifecycle", event, "observed", {
|
||||
target: targetId,
|
||||
}),
|
||||
);
|
||||
patchWebContents(window.webContents, targetId);
|
||||
return window;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const boundaries = createElectronActiveBoundaryPatches({
|
||||
identity,
|
||||
patchEmitter,
|
||||
processType,
|
||||
recordIpc,
|
||||
recordRuntime,
|
||||
shape,
|
||||
});
|
||||
|
||||
try {
|
||||
const electron = require("electron");
|
||||
patchEmitter(electron.app, (event, args) =>
|
||||
recordRuntime(
|
||||
event === "open-url" || event === "second-instance"
|
||||
? "protocol"
|
||||
: "app-lifecycle",
|
||||
event,
|
||||
"observed",
|
||||
{
|
||||
target: "app",
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
process_type: "main",
|
||||
},
|
||||
),
|
||||
);
|
||||
patchIpcMain(electron.ipcMain);
|
||||
boundaries.patchIpcRenderer(electron.ipcRenderer);
|
||||
boundaries.patchContextBridge(electron.contextBridge);
|
||||
patchBrowserWindow(electron);
|
||||
boundaries.patchUtilityProcess(electron.utilityProcess);
|
||||
boundaries.patchShell(electron.shell);
|
||||
boundaries.patchSessionManager(electron.session);
|
||||
boundaries.patchApplicationEffects(electron.app);
|
||||
boundaries.patchNativeAddonLoading();
|
||||
boundaries.patchChildProcess();
|
||||
process.once("uncaughtException", (cause, origin) => {
|
||||
recordRuntime("process-lifecycle", "uncaught-exception", "observed", {
|
||||
argument_shapes: [shape(cause), shape(origin)],
|
||||
error: true,
|
||||
});
|
||||
throw cause;
|
||||
});
|
||||
process.once("unhandledRejection", (reason, promise) => {
|
||||
recordRuntime("process-lifecycle", "unhandled-rejection", "observed", {
|
||||
argument_shapes: [shape(reason), shape(promise)],
|
||||
error: true,
|
||||
});
|
||||
throw reason;
|
||||
});
|
||||
} catch {
|
||||
globalThis.__reaElectronActiveHookError = true;
|
||||
}
|
||||
|
||||
globalThis.__reaElectronActiveSnapshot = () => ({
|
||||
events: events.slice(),
|
||||
observed,
|
||||
observed_ipc: observedIpc,
|
||||
observed_runtime: observedRuntime,
|
||||
hook_error: globalThis.__reaElectronActiveHookError === true,
|
||||
});
|
||||
@@ -7,6 +7,21 @@ export async function startBrowserVerifierSite() {
|
||||
let port = 0;
|
||||
let sessionGeneration = 0;
|
||||
const server = createServer((request, response) => {
|
||||
if (request.url === "/screenshot-noise") {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(`<!doctype html><html><body style="margin:0"><canvas width="2048" height="1536"></canvas><script>
|
||||
const context = document.querySelector('canvas').getContext('2d');
|
||||
const image = context.createImageData(2048, 1536);
|
||||
let state = 0x6d2b79f5;
|
||||
for (let index = 0; index < image.data.length; index++) {
|
||||
state ^= state << 13; state ^= state >>> 17; state ^= state << 5;
|
||||
image.data[index] = index % 4 === 3 ? 255 : state & 255;
|
||||
}
|
||||
context.putImageData(image, 0, 0);
|
||||
document.body.dataset.ready = 'true';
|
||||
</script></body></html>`);
|
||||
return;
|
||||
}
|
||||
if (request.url === "/app.js.map") {
|
||||
response.setHeader("content-type", "application/source-map+json");
|
||||
response.end(sourceMap());
|
||||
|
||||
@@ -30,16 +30,27 @@ const { stdout } = await exec(
|
||||
},
|
||||
);
|
||||
const verifierOutput = JSON.parse(stdout);
|
||||
// Managed conformance claims are host-independent. Keep the checked-in
|
||||
// commitment reproducible across macOS, Linux, and Windows rather than
|
||||
// embedding the machine that happened to generate it.
|
||||
const portableCompletionReport = {
|
||||
...verifierOutput.completionReport,
|
||||
environment: {
|
||||
platform: "portable",
|
||||
architecture: "portable",
|
||||
runtime: "node",
|
||||
runtime_version: "portable",
|
||||
},
|
||||
};
|
||||
const { createCompletionLedgerArtifacts } = await import(
|
||||
`../dist/domain/completionLedgerGeneration.js?${String(Date.now())}`
|
||||
);
|
||||
const skillDigest = await directoryDigest(
|
||||
join(root, "skills/reverse-engineer-anything"),
|
||||
);
|
||||
const generated = createCompletionLedgerArtifacts(
|
||||
verifierOutput.completionReport,
|
||||
[{ skill_id: "reverse-engineer-anything", sha256: skillDigest }],
|
||||
);
|
||||
const generated = createCompletionLedgerArtifacts(portableCompletionReport, [
|
||||
{ skill_id: "reverse-engineer-anything", sha256: skillDigest },
|
||||
]);
|
||||
|
||||
if (!check) await mkdir(outputRoot, { recursive: true });
|
||||
await Promise.all([
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { format } from "prettier";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
|
||||
import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
|
||||
import { ManagedStaticProvider } from "../dist/dotnet/ManagedStaticProvider.js";
|
||||
import { NativeMacOSProvider } from "../dist/native/NativeMacOSProvider.js";
|
||||
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
||||
const arguments_ = new Set(process.argv.slice(2));
|
||||
for (const argument of arguments_)
|
||||
if (argument !== "--check")
|
||||
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
// The checked-in catalog is a portable package artifact. Keep its canonical
|
||||
// provider metadata independent of the machine running the generator.
|
||||
const CATALOG_PLATFORM = "darwin";
|
||||
const sessionToolNames = new Set([
|
||||
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
|
||||
({ name }) => name,
|
||||
),
|
||||
...MANAGED_WORKFLOW_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
]);
|
||||
const advertisedTools = await sdkToolCatalog();
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
for (const tool of advertisedTools.values()) {
|
||||
for (const kind of ["inputSchema", "outputSchema"]) {
|
||||
const schema = tool[kind];
|
||||
if (schema !== undefined && !ajv.validateSchema(schema))
|
||||
throw new Error(
|
||||
`Invalid JSON Schema for ${tool.name}.${kind}: ${ajv.errorsText(ajv.errors)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const catalog = TOOL_CONTRACTS.map((contract) => {
|
||||
const advertised = advertisedTools.get(contract.name);
|
||||
if (advertised === undefined)
|
||||
throw new Error(`MCP SDK omitted registered tool ${contract.name}`);
|
||||
return {
|
||||
name: contract.name,
|
||||
analysisOperation: [
|
||||
"official-proxy",
|
||||
"enhanced",
|
||||
"native-provider",
|
||||
"artifact-provider",
|
||||
"managed-provider",
|
||||
].includes(contract.kind)
|
||||
? contract.name
|
||||
: null,
|
||||
title: advertised.title,
|
||||
description: advertised.description,
|
||||
kind: contract.kind,
|
||||
requiresSession: sessionToolNames.has(contract.name),
|
||||
inputSchema: advertised.inputSchema,
|
||||
outputSchema: advertised.outputSchema,
|
||||
annotations: advertised.annotations,
|
||||
effects: contract.effects,
|
||||
};
|
||||
});
|
||||
|
||||
async function sdkToolCatalog() {
|
||||
const server = new McpServer({ name: "rea-catalog-generator", version: "0" });
|
||||
for (const contract of TOOL_CONTRACTS)
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
async () => ({
|
||||
content: [{ type: "text", text: "Catalog-only handler" }],
|
||||
isError: true,
|
||||
}),
|
||||
);
|
||||
const client = new Client({ name: "rea-catalog-generator", version: "0" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
try {
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const { tools } = await client.listTools();
|
||||
return new Map(tools.map((tool) => [tool.name, tool]));
|
||||
} finally {
|
||||
await Promise.allSettled([client.close(), server.close()]);
|
||||
}
|
||||
}
|
||||
const auxiliaryProviders = [
|
||||
new ArtifactProvider(false, false, CATALOG_PLATFORM),
|
||||
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
|
||||
new ManagedStaticProvider(),
|
||||
].map((provider) => ({
|
||||
identity: provider.identity(),
|
||||
capabilities: provider.capabilities(),
|
||||
}));
|
||||
|
||||
const canonicalizeJson = (value) => {
|
||||
if (Array.isArray(value)) return value.map(canonicalizeJson);
|
||||
if (value === null || typeof value !== "object") return value;
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
||||
.map(([key, entry]) => [key, canonicalizeJson(entry)]),
|
||||
);
|
||||
};
|
||||
|
||||
const payloadJson = JSON.stringify(
|
||||
canonicalizeJson({ catalog, auxiliaryProviders }),
|
||||
);
|
||||
const source = await format(
|
||||
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
|
||||
import type {
|
||||
CapabilityDescriptor,
|
||||
ProviderIdentity,
|
||||
} from "./application/AnalysisProvider.js";
|
||||
import type { ToolKind } from "./contracts/toolContractTypes.js";
|
||||
import type { ToolEffects } from "./contracts/toolEffects.js";
|
||||
|
||||
interface GeneratedTool {
|
||||
readonly name: string;
|
||||
readonly analysisOperation: CapabilityDescriptor["operation"] | null;
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly kind: ToolKind;
|
||||
readonly requiresSession: boolean;
|
||||
readonly inputSchema: Readonly<Record<string, unknown>>;
|
||||
readonly outputSchema: Readonly<Record<string, unknown>>;
|
||||
readonly annotations: ToolAnnotations;
|
||||
readonly effects: ToolEffects;
|
||||
}
|
||||
|
||||
interface GeneratedProvider {
|
||||
readonly identity: ProviderIdentity;
|
||||
readonly capabilities: readonly CapabilityDescriptor[];
|
||||
}
|
||||
|
||||
interface GeneratedPayload {
|
||||
readonly catalog: readonly GeneratedTool[];
|
||||
readonly auxiliaryProviders: readonly GeneratedProvider[];
|
||||
}
|
||||
|
||||
const GENERATED_PAYLOAD_JSON = ${JSON.stringify(payloadJson)};
|
||||
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
|
||||
if (!isGeneratedPayload(generatedPayload))
|
||||
throw new TypeError("Generated MCP catalog payload is invalid");
|
||||
|
||||
/** Generated from TOOL_CONTRACTS; do not edit. */
|
||||
export const GENERATED_MCP_TOOL_CATALOG = generatedPayload.catalog;
|
||||
|
||||
/** Generated lightweight metadata for lazily loaded auxiliary providers. */
|
||||
export const GENERATED_AUXILIARY_PROVIDERS = generatedPayload.auxiliaryProviders;
|
||||
|
||||
function isGeneratedPayload(value: unknown): value is GeneratedPayload {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
Array.isArray(value.catalog) &&
|
||||
value.catalog.every(isGeneratedTool) &&
|
||||
Array.isArray(value.auxiliaryProviders) &&
|
||||
value.auxiliaryProviders.every(isGeneratedProvider)
|
||||
);
|
||||
}
|
||||
|
||||
function isGeneratedTool(value: unknown): value is GeneratedTool {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.name === "string" &&
|
||||
(typeof value.analysisOperation === "string" ||
|
||||
value.analysisOperation === null) &&
|
||||
typeof value.title === "string" &&
|
||||
typeof value.description === "string" &&
|
||||
typeof value.kind === "string" &&
|
||||
typeof value.requiresSession === "boolean" &&
|
||||
isRecord(value.inputSchema) &&
|
||||
isRecord(value.outputSchema) &&
|
||||
isRecord(value.annotations) &&
|
||||
isRecord(value.effects)
|
||||
);
|
||||
}
|
||||
|
||||
function isGeneratedProvider(value: unknown): value is GeneratedProvider {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
isRecord(value.identity) &&
|
||||
Array.isArray(value.capabilities)
|
||||
);
|
||||
}
|
||||
|
||||
function isRecord(
|
||||
value: unknown,
|
||||
): value is Readonly<Record<string, unknown>> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
`,
|
||||
{ parser: "typescript" },
|
||||
);
|
||||
|
||||
await ensureGeneratedFile({
|
||||
path: join(root, "src/generatedMcpToolCatalog.ts"),
|
||||
source,
|
||||
check: arguments_.has("--check"),
|
||||
generateCommand: "npm run mcp-catalog:generate",
|
||||
});
|
||||
+13
-32
@@ -36,11 +36,8 @@ UNEXPECTED_DISPLAY_GEOMETRY = 77
|
||||
X11_INPUT_FAILED = 78
|
||||
RUNTIME_DEPENDENCY_UNAVAILABLE = 79
|
||||
X11_SOCKET_DIRECTORY_UNUSABLE = 80
|
||||
DIAGNOSTIC_PREFIX = "REA_X11_DIAGNOSTIC_V1="
|
||||
DIAGNOSTIC_PREFIX = "REA_X11_DIAGNOSTIC="
|
||||
X11_SOCKET_DIRECTORY = Path("/tmp/.X11-unix")
|
||||
XVFB_STDERR_LIMIT = 16 * 1024
|
||||
|
||||
|
||||
class AdapterFailure(Exception):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -48,20 +45,18 @@ class AdapterFailure(Exception):
|
||||
reason: str,
|
||||
message: str,
|
||||
stderr_bytes: int = 0,
|
||||
stderr_truncated: bool = False,
|
||||
) -> None:
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
self.reason = reason
|
||||
self.stderr_bytes = stderr_bytes
|
||||
self.stderr_truncated = stderr_truncated
|
||||
|
||||
def with_stderr(self, capture: BoundedStderr) -> AdapterFailure:
|
||||
self.stderr_bytes, self.stderr_truncated, _ = capture.snapshot()
|
||||
def with_stderr(self, capture: CapturedStderr) -> AdapterFailure:
|
||||
self.stderr_bytes, _ = capture.snapshot()
|
||||
return self
|
||||
|
||||
|
||||
class BoundedStderr:
|
||||
class CapturedStderr:
|
||||
def __init__(self, stream: object) -> None:
|
||||
self._stream = stream
|
||||
self._retained = bytearray()
|
||||
@@ -77,15 +72,13 @@ class BoundedStderr:
|
||||
return
|
||||
with self._lock:
|
||||
self._bytes += len(chunk)
|
||||
remaining = XVFB_STDERR_LIMIT - len(self._retained)
|
||||
if remaining > 0:
|
||||
self._retained.extend(chunk[:remaining])
|
||||
self._retained.extend(chunk)
|
||||
|
||||
def snapshot(self) -> tuple[int, bool, str]:
|
||||
def snapshot(self) -> tuple[int, str]:
|
||||
with self._lock:
|
||||
retained = bytes(self._retained)
|
||||
total = self._bytes
|
||||
return total, total > len(retained), retained.decode("utf-8", "replace")
|
||||
return total, retained.decode("utf-8", "replace")
|
||||
|
||||
def close(self) -> None:
|
||||
self._thread.join(timeout=0.5)
|
||||
@@ -284,14 +277,11 @@ def diagnostic(
|
||||
operation: str,
|
||||
status: str,
|
||||
failure: AdapterFailure | None = None,
|
||||
capture: BoundedStderr | None = None,
|
||||
capture: CapturedStderr | None = None,
|
||||
) -> dict[str, object]:
|
||||
mode, read_only, _ = socket_directory_state()
|
||||
captured_bytes, captured_truncated, _ = (
|
||||
(0, False, "") if capture is None else capture.snapshot()
|
||||
)
|
||||
captured_bytes, _ = (0, "") if capture is None else capture.snapshot()
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"component": "hopper_private_display",
|
||||
"operation": operation,
|
||||
"status": status,
|
||||
@@ -308,9 +298,6 @@ def diagnostic(
|
||||
"xvfb_stderr_bytes": captured_bytes
|
||||
if failure is None
|
||||
else failure.stderr_bytes,
|
||||
"xvfb_stderr_truncated": captured_truncated
|
||||
if failure is None
|
||||
else failure.stderr_truncated,
|
||||
}
|
||||
|
||||
|
||||
@@ -441,8 +428,8 @@ def authorize_display(
|
||||
) from error
|
||||
|
||||
|
||||
def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
|
||||
stderr_bytes, stderr_truncated, text = capture.snapshot()
|
||||
def classify_xvfb_failure(capture: CapturedStderr) -> AdapterFailure:
|
||||
stderr_bytes, text = capture.snapshot()
|
||||
mode, read_only, directory = socket_directory_state()
|
||||
if read_only is True:
|
||||
return AdapterFailure(
|
||||
@@ -450,7 +437,6 @@ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
|
||||
"socket_directory_read_only",
|
||||
"X11 socket directory is read-only",
|
||||
stderr_bytes,
|
||||
stderr_truncated,
|
||||
)
|
||||
if directory and mode != "1777":
|
||||
return AdapterFailure(
|
||||
@@ -458,7 +444,6 @@ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
|
||||
"socket_directory_mode",
|
||||
"X11 socket directory mode is not 1777",
|
||||
stderr_bytes,
|
||||
stderr_truncated,
|
||||
)
|
||||
if mode is not None and not directory:
|
||||
return AdapterFailure(
|
||||
@@ -466,7 +451,6 @@ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
|
||||
"socket_directory_type",
|
||||
"X11 socket path is not a directory",
|
||||
stderr_bytes,
|
||||
stderr_truncated,
|
||||
)
|
||||
lowered = text.lower()
|
||||
if "error while loading shared libraries" in lowered:
|
||||
@@ -475,7 +459,6 @@ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
|
||||
"missing_library",
|
||||
"Xvfb shared library unavailable",
|
||||
stderr_bytes,
|
||||
stderr_truncated,
|
||||
)
|
||||
if any(
|
||||
marker in lowered
|
||||
@@ -490,21 +473,19 @@ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
|
||||
"address_collision",
|
||||
"private display address collision",
|
||||
stderr_bytes,
|
||||
stderr_truncated,
|
||||
)
|
||||
return AdapterFailure(
|
||||
PRIVATE_DISPLAY_UNAVAILABLE,
|
||||
"xvfb_startup_failed",
|
||||
"private X display unavailable",
|
||||
stderr_bytes,
|
||||
stderr_truncated,
|
||||
)
|
||||
|
||||
|
||||
def start_xvfb(
|
||||
session: Path,
|
||||
args: argparse.Namespace,
|
||||
) -> tuple[subprocess.Popen[bytes], str, Path, BoundedStderr]:
|
||||
) -> tuple[subprocess.Popen[bytes], str, Path, CapturedStderr]:
|
||||
authority = session / "Xauthority"
|
||||
collision: AdapterFailure | None = None
|
||||
for _ in range(3):
|
||||
@@ -551,7 +532,7 @@ def start_xvfb(
|
||||
"xvfb_startup_failed",
|
||||
"Xvfb stderr unavailable",
|
||||
)
|
||||
capture = BoundedStderr(process.stderr)
|
||||
capture = CapturedStderr(process.stderr)
|
||||
deadline = time.monotonic() + 2
|
||||
socket = Path(f"/tmp/.X11-unix/X{display_number}")
|
||||
while time.monotonic() < deadline and process.poll() is None:
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
const environment = () => ({
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "auto",
|
||||
// Artifact workflows have no deep-provider prerequisite or GUI authority.
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
});
|
||||
|
||||
/** Execute the compiled CLI with production providers and no test adapters. */
|
||||
export async function artifactCli(command, target, arguments_ = []) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
||||
command,
|
||||
target,
|
||||
...arguments_,
|
||||
"--json",
|
||||
],
|
||||
{ env: environment(), timeout: 60000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = JSON.parse(stdout);
|
||||
assert.equal(evidence.error, undefined, stdout);
|
||||
assert.ok(evidence.normalized_result, "CLI omitted normalized result");
|
||||
return evidence.normalized_result;
|
||||
}
|
||||
|
||||
/** Run a real stdio MCP subprocess, owning and closing its transport. */
|
||||
export async function withArtifactMcp(target, verify) {
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
|
||||
env: environment(),
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "artifact-real-e2e", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const opened = await client.callTool({
|
||||
name: "open_binary",
|
||||
arguments: { path: target },
|
||||
});
|
||||
assert.notEqual(opened.isError, true, JSON.stringify(opened));
|
||||
await verify(client);
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Require a successful structured MCP tool result rather than its text rendering. */
|
||||
export async function artifactMcpResult(client, name, arguments_ = {}) {
|
||||
const result = await client.callTool({ name, arguments: arguments_ });
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
assert.ok(result.structuredContent?.result, "MCP omitted structured result");
|
||||
return result.structuredContent.result;
|
||||
}
|
||||
@@ -42,7 +42,6 @@ const duplicateOriginStorage = (origin) => ({
|
||||
/** Build the source-owned full-capture browser scenario. */
|
||||
export function browserScenario(browser, origin) {
|
||||
return browserScenarioSchema.parse({
|
||||
schema_version: 1,
|
||||
browser,
|
||||
start_url: { url: `${origin}/app`, query: [] },
|
||||
allowed_origins: [origin],
|
||||
@@ -112,23 +111,6 @@ export function browserScenario(browser, origin) {
|
||||
"downloads",
|
||||
],
|
||||
},
|
||||
limits: {
|
||||
max_duration_ms: 30_000,
|
||||
action_timeout_ms: 5_000,
|
||||
navigation_timeout_ms: 10_000,
|
||||
max_events: 10_000,
|
||||
max_frames: 100,
|
||||
max_workers: 20,
|
||||
max_popups: 10,
|
||||
max_websockets: 100,
|
||||
max_dom_nodes: 10_000,
|
||||
max_accessibility_nodes: 10_000,
|
||||
max_screenshots: 8,
|
||||
max_screenshot_bytes: 4 * 1_024 * 1_024,
|
||||
max_storage_entries: 256,
|
||||
max_total_metadata_bytes: 8 * 1_024 * 1_024,
|
||||
},
|
||||
approved: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { chromium } from "playwright-core";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { webScreenshotSchema } from "../../dist/domain/webScreenshot.js";
|
||||
import { comparePngScreenshots } from "../../dist/browser/PngVisualDiff.js";
|
||||
|
||||
/** Verify a real, large PNG through production CLI and stdio MCP boundaries. */
|
||||
export async function verifyLargeScreenshotE2e(endpoint, origin) {
|
||||
const browser = await chromium.connectOverCDP(endpoint);
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 2048, height: 1536 },
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const env = {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
REA_BROWSER_OBSERVE_ENABLED: "true",
|
||||
REA_BROWSER_CDP_ENDPOINTS_JSON: JSON.stringify([endpoint]),
|
||||
REA_BROWSER_ALLOWED_ORIGINS_JSON: JSON.stringify([origin]),
|
||||
};
|
||||
let client;
|
||||
let transport;
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
await page.goto(`${origin}/screenshot-noise`);
|
||||
await page.waitForFunction(() => document.body.dataset.ready === "true");
|
||||
const targets = await (await fetch(`${endpoint}/json/list`)).json();
|
||||
const target = targets.find((value) => value.url === page.url());
|
||||
assert.ok(
|
||||
target,
|
||||
"Owned screenshot page is absent from actual CDP discovery",
|
||||
);
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[entrypoint, "capture-web-screenshot", endpoint, target.id, "--json"],
|
||||
{ env, timeout: 60000, maxBuffer: 72 * 1024 * 1024 },
|
||||
);
|
||||
const cliEvidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(cliEvidence.operation, "capture_web_screenshot");
|
||||
const screenshot = webScreenshotSchema.parse(cliEvidence.normalized_result);
|
||||
assert.ok(
|
||||
screenshot.artifact.bytes > 8 * 1024 * 1024,
|
||||
"Real PNG did not cross the former screenshot byte ceiling",
|
||||
);
|
||||
const bytes = Buffer.from(screenshot.artifact.data_base64, "base64");
|
||||
assert.equal(bytes.byteLength, screenshot.artifact.bytes);
|
||||
assert.deepEqual(
|
||||
bytes.subarray(0, 8),
|
||||
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
||||
);
|
||||
assert.equal(bytes.readUInt32BE(16), 2048);
|
||||
assert.equal(bytes.readUInt32BE(20), 1536);
|
||||
transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
// The SDK defaults to a 10 MiB receive buffer; inline PNG output is larger.
|
||||
maxBufferSize: 64 * 1024 * 1024,
|
||||
});
|
||||
client = new Client({ name: "browser-screenshot-real-e2e", version: "1" });
|
||||
await client.connect(transport);
|
||||
const captured = await client.callTool(
|
||||
{
|
||||
name: "capture_web_screenshot",
|
||||
arguments: {
|
||||
cdp_endpoint: endpoint,
|
||||
allowed_origins: [origin],
|
||||
target_id: target.id,
|
||||
},
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
assert.notEqual(captured.isError, true, JSON.stringify(captured));
|
||||
const mcpEvidence = parseEvidence({
|
||||
...captured.structuredContent?.evidence,
|
||||
normalized_result: captured.structuredContent?.result,
|
||||
});
|
||||
const mcpScreenshot = webScreenshotSchema.parse(
|
||||
mcpEvidence.normalized_result,
|
||||
);
|
||||
assert.deepEqual(mcpScreenshot.artifact, screenshot.artifact);
|
||||
const compared = comparePngScreenshots({
|
||||
before: screenshot.artifact,
|
||||
after: mcpScreenshot.artifact,
|
||||
channel_threshold: 0,
|
||||
});
|
||||
assert.equal(compared.status, "identical");
|
||||
assert.equal(compared.changed_pixels, 0);
|
||||
return {
|
||||
mocked: false,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
png_bytes: bytes.byteLength,
|
||||
};
|
||||
} finally {
|
||||
try {
|
||||
if (client !== undefined) await client.close();
|
||||
} finally {
|
||||
try {
|
||||
if (transport !== undefined) await transport.close();
|
||||
} finally {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,26 +81,12 @@ export const toolFamilyCatalog = (sources) => {
|
||||
|
||||
/** Build provider identities with their sorted capability names. */
|
||||
export const providerCatalog = (sources) => {
|
||||
const applicationContracts =
|
||||
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "analyze_javascript_application",
|
||||
);
|
||||
const reconciliationContracts =
|
||||
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "reconcile_javascript_runtime",
|
||||
);
|
||||
const observationContracts =
|
||||
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
||||
({ name }) =>
|
||||
name !== "analyze_javascript_application" &&
|
||||
name !== "reconcile_javascript_runtime",
|
||||
);
|
||||
if (
|
||||
applicationContracts.length !== 1 ||
|
||||
reconciliationContracts.length !== 1 ||
|
||||
observationContracts.length !== 2
|
||||
)
|
||||
throw new Error("Electron provider capability ownership drifted");
|
||||
const {
|
||||
applicationContracts,
|
||||
reconciliationContracts,
|
||||
observationContracts,
|
||||
activeContracts,
|
||||
} = electronContractSlices(sources);
|
||||
return [
|
||||
{
|
||||
identity: sources.hopperProvider.HOPPER_PROVIDER_IDENTITY,
|
||||
@@ -142,6 +128,12 @@ export const providerCatalog = (sources) => {
|
||||
identity: sources.electronProvider.CDP_ELECTRON_PROVIDER_IDENTITY,
|
||||
contracts: observationContracts,
|
||||
},
|
||||
{
|
||||
identity:
|
||||
sources.electronActiveProvider
|
||||
.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
|
||||
contracts: activeContracts,
|
||||
},
|
||||
{
|
||||
identity: sources.v8InspectorProvider.V8_INSPECTOR_PROVIDER_IDENTITY,
|
||||
contracts:
|
||||
@@ -171,3 +163,38 @@ export const providerCatalog = (sources) => {
|
||||
}))
|
||||
.sort((left, right) => left.id.localeCompare(right.id));
|
||||
};
|
||||
|
||||
const electronContractSlices = (sources) => {
|
||||
const applicationContracts =
|
||||
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "analyze_javascript_application",
|
||||
);
|
||||
const reconciliationContracts =
|
||||
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "reconcile_javascript_runtime",
|
||||
);
|
||||
const observationContracts =
|
||||
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
||||
({ name }) =>
|
||||
name !== "analyze_javascript_application" &&
|
||||
name !== "reconcile_javascript_runtime" &&
|
||||
name !== "capture_electron_scenario",
|
||||
);
|
||||
const activeContracts =
|
||||
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "capture_electron_scenario",
|
||||
);
|
||||
if (
|
||||
applicationContracts.length !== 1 ||
|
||||
reconciliationContracts.length !== 1 ||
|
||||
observationContracts.length !== 2 ||
|
||||
activeContracts.length !== 1
|
||||
)
|
||||
throw new Error("Electron provider capability ownership drifted");
|
||||
return {
|
||||
applicationContracts,
|
||||
reconciliationContracts,
|
||||
observationContracts,
|
||||
activeContracts,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
const load = (root, path) => import(pathToFileURL(join(root, path)).href);
|
||||
|
||||
/** Sort tool/provider names alphabetically. */
|
||||
@@ -9,6 +12,14 @@ export const names = (contracts) =>
|
||||
.map(({ name }) => name)
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
|
||||
/** Return the stable digest for a generated catalog projection. */
|
||||
export const digest = (value) => {
|
||||
const encoded = canonicalize(value);
|
||||
if (encoded === undefined)
|
||||
throw new TypeError("Catalog projection is not canonical JSON");
|
||||
return createHash("sha256").update(encoded).digest("hex");
|
||||
};
|
||||
|
||||
/** Throw when two name lists diverge. */
|
||||
export const assertSameNames = (label, actual, expected) => {
|
||||
const actualSet = new Set(actual);
|
||||
@@ -44,6 +55,7 @@ const SOURCE_PATHS = {
|
||||
browserProvider: "dist/browser/CdpBrowserProvider.js",
|
||||
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
|
||||
electronProvider: "dist/browser/CdpElectronProvider.js",
|
||||
electronActiveProvider: "dist/browser/PlaywrightElectronActiveProvider.js",
|
||||
v8InspectorProvider: "dist/browser/V8InspectorProvider.js",
|
||||
evidence: "dist/domain/evidence.js",
|
||||
evidenceBundle: "dist/domain/evidenceBundle.js",
|
||||
@@ -52,13 +64,13 @@ const SOURCE_PATHS = {
|
||||
processCapture: "dist/domain/processCapture.js",
|
||||
analysisSnapshot: "dist/domain/analysisSnapshot.js",
|
||||
artifactGraph: "dist/domain/artifactGraph.js",
|
||||
investigationWorkspace: "dist/domain/investigationWorkspace.js",
|
||||
browserObservation: "dist/domain/browserObservation.js",
|
||||
browserScenario: "dist/domain/browserScenario.js",
|
||||
browserScenarioCapture: "dist/domain/browserScenarioCapture.js",
|
||||
browserScenarioDiff: "dist/domain/browserScenarioDiff.js",
|
||||
browserSession: "dist/domain/browserSession.js",
|
||||
electronObservation: "dist/domain/electronObservation.js",
|
||||
electronActiveObservation: "dist/domain/electronActiveObservation.js",
|
||||
javascriptRuntimeObservation: "dist/domain/javascriptRuntimeObservation.js",
|
||||
webBundleAnalysis: "dist/domain/webBundleAnalysis.js",
|
||||
webCaptureDiff: "dist/domain/webCaptureDiff.js",
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const valuesAt = (node, path) => {
|
||||
if (typeof node !== "object" || node === null) return [];
|
||||
if ("anyOf" in node && Array.isArray(node.anyOf))
|
||||
return node.anyOf.flatMap((alternative) => valuesAt(alternative, path));
|
||||
if (path.length === 0)
|
||||
return "const" in node &&
|
||||
(typeof node.const === "number" || typeof node.const === "string")
|
||||
? [node.const]
|
||||
: [];
|
||||
if (
|
||||
!("properties" in node) ||
|
||||
typeof node.properties !== "object" ||
|
||||
node.properties === null
|
||||
)
|
||||
return [];
|
||||
const [segment, ...remaining] = path;
|
||||
return segment !== undefined && segment in node.properties
|
||||
? valuesAt(node.properties[segment], remaining)
|
||||
: [];
|
||||
};
|
||||
|
||||
const versionAt = (schema, path) => {
|
||||
const versions = [
|
||||
...new Set(
|
||||
valuesAt(z.toJSONSchema(schema, { unrepresentable: "any" }), path).map(
|
||||
String,
|
||||
),
|
||||
),
|
||||
];
|
||||
if (versions.length !== 1)
|
||||
throw new Error(
|
||||
`Schema version path ${path.join(".")} has ${String(versions.length)} constant values`,
|
||||
);
|
||||
const version = versions[0];
|
||||
if (version === undefined) throw new Error("Schema version disappeared");
|
||||
return /^\d+$/u.test(version) ? Number(version) : version;
|
||||
};
|
||||
|
||||
const durableSchemaDefinitions = (sources) => [
|
||||
[
|
||||
"analysis_snapshot",
|
||||
sources.analysisSnapshot.analysisSnapshotSchema,
|
||||
["snapshot_version"],
|
||||
],
|
||||
[
|
||||
"artifact_graph",
|
||||
sources.artifactGraph.artifactInventoryResultSchema,
|
||||
["manifest", "schema_version"],
|
||||
],
|
||||
[
|
||||
"artifact_extraction",
|
||||
sources.artifactGraph.artifactExtractionResultSchema,
|
||||
["extraction_manifest", "schema_version"],
|
||||
],
|
||||
[
|
||||
"browser_scenario",
|
||||
sources.browserScenario.browserScenarioSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
["evidence", sources.evidence.evidenceSchema, ["schema_version"]],
|
||||
[
|
||||
"evidence_bundle",
|
||||
sources.evidenceBundle.evidenceBundleSchema,
|
||||
["bundle_version"],
|
||||
],
|
||||
[
|
||||
"evidence_completion_ledger",
|
||||
sources.evidenceCompletion.evidenceCompletionLedgerSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"evidence_completion_manifest",
|
||||
sources.completionGeneration.completionManifestSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"investigation_run",
|
||||
sources.investigationWorkspace.investigationRunSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"investigation_run_summary",
|
||||
sources.investigationWorkspace.investigationRunSummarySchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"investigation_workspace",
|
||||
sources.investigationWorkspace.investigationWorkspaceSchema,
|
||||
["workspace_version"],
|
||||
],
|
||||
[
|
||||
"javascript_application_graph",
|
||||
sources.javascriptApplicationGraph.javascriptApplicationGraphSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"javascript_application_version_comparison",
|
||||
sources.javascriptVersionComparison
|
||||
.applicationVersionComparisonResultSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"javascript_semantic_graph",
|
||||
sources.javascriptSemanticGraph.javaScriptSemanticGraphSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"javascript_semantic_query",
|
||||
sources.javascriptSemanticQuery.javaScriptSemanticQueryResultSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"javascript_semantic_trace",
|
||||
sources.javascriptSemanticTrace.javaScriptSemanticTraceResultSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"javascript_feature_trace",
|
||||
sources.javascriptFeatureTrace.applicationFeatureTraceResultSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"process_capture",
|
||||
sources.processCapture.processCaptureSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"reconstruction_verification",
|
||||
sources.reconstructionVerification.reconstructionSpecificationSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"residual_unknown",
|
||||
sources.residualUnknown.residualUnknownSchema,
|
||||
["registry_version"],
|
||||
],
|
||||
];
|
||||
|
||||
const browserScenarioObservationSchemaDefinitions = (sources) => [
|
||||
[
|
||||
"browser_scenario_capture",
|
||||
sources.browserScenarioCapture.browserScenarioCaptureSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"browser_scenario_diff",
|
||||
sources.browserScenarioDiff.browserScenarioDiffSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
];
|
||||
|
||||
const observationSchemaDefinitions = (sources) => [
|
||||
[
|
||||
"managed_artifact_inspection",
|
||||
sources.managedArtifact.managedArtifactInspectionSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"managed_member_inspection",
|
||||
sources.managedArtifact.managedMemberInspectionSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"managed_native_boundary_inspection",
|
||||
sources.managedArtifact.managedNativeBoundaryInspectionSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"managed_member_comparison",
|
||||
sources.managedComparison.managedMemberComparisonResultSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"managed_native_verification",
|
||||
sources.managedNativeVerification.managedNativeVerificationResultSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"browser_target_list",
|
||||
sources.browserObservation.browserTargetListSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
...browserScenarioObservationSchemaDefinitions(sources),
|
||||
[
|
||||
"electron_page_inspection",
|
||||
sources.electronObservation.electronPageInspectionSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"electron_target_list",
|
||||
sources.electronObservation.electronTargetListSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"javascript_application_analysis",
|
||||
sources.javascriptApplicationAnalysis
|
||||
.javaScriptApplicationAnalysisResultV2Schema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"javascript_runtime_observation",
|
||||
sources.javascriptRuntimeObservation.javascriptRuntimeObservationSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"javascript_runtime_target_list",
|
||||
sources.javascriptRuntimeObservation.javascriptRuntimeTargetListSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"web_bundle_analysis",
|
||||
sources.webBundleAnalysis.webBundleAnalysisSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"web_capture_diff",
|
||||
sources.webCaptureDiff.webCaptureDiffSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"web_mcp_discovery",
|
||||
sources.webMcpDiscovery.webMcpDiscoverySchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"web_observation_session",
|
||||
sources.browserSession.webObservationSessionSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"web_page_inspection",
|
||||
sources.browserObservation.webPageInspectionSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"web_screenshot",
|
||||
sources.webScreenshot.webScreenshotSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
[
|
||||
"web_screenshot_diff",
|
||||
sources.webScreenshot.webScreenshotDiffSchema,
|
||||
["schema_version"],
|
||||
],
|
||||
];
|
||||
|
||||
/** Derive the catalog of durable and observation schema versions. */
|
||||
export const schemaCatalog = (sources) =>
|
||||
[
|
||||
...durableSchemaDefinitions(sources),
|
||||
...observationSchemaDefinitions(sources),
|
||||
]
|
||||
.map(([id, schema, path]) => {
|
||||
try {
|
||||
return { id, version: versionAt(schema, path) };
|
||||
} catch (cause) {
|
||||
throw new Error(`Could not derive ${id} schema version`, { cause });
|
||||
}
|
||||
})
|
||||
.sort((left, right) => left.id.localeCompare(right.id));
|
||||
@@ -9,7 +9,7 @@ export const HOPPER_C_ORACLE: Readonly<{
|
||||
globalName: "rea_c_global";
|
||||
}>;
|
||||
export function sha256(value: string | Uint8Array): string;
|
||||
export function generateLargeFixture(count?: number): string;
|
||||
export function generateLargeFixture(): string;
|
||||
export function sourceDigest(
|
||||
sources: readonly { readonly path: string; readonly content: string }[],
|
||||
): string;
|
||||
|
||||
@@ -15,18 +15,16 @@ export const HOPPER_C_ORACLE = Object.freeze({
|
||||
export const sha256 = (value) =>
|
||||
createHash("sha256").update(value).digest("hex");
|
||||
|
||||
export const generateLargeFixture = (count = LARGE_FIXTURE_COUNT) => {
|
||||
if (!Number.isInteger(count) || count < 1)
|
||||
throw new Error("Fixture count must be a positive integer");
|
||||
export const generateLargeFixture = () => {
|
||||
const declarations = Array.from(
|
||||
{ length: count },
|
||||
{ length: LARGE_FIXTURE_COUNT },
|
||||
(_, index) =>
|
||||
`__attribute__((noinline, used)) int rea_page_${String(index).padStart(4, "0")}(void) { puts("REA_PAGE_${String(index).padStart(4, "0")}"); return ${index}; }`,
|
||||
`__attribute__((noinline, used)) int rea_inventory_${String(index).padStart(4, "0")}(void) { puts("REA_INVENTORY_${String(index).padStart(4, "0")}"); return ${index}; }`,
|
||||
);
|
||||
return [
|
||||
"#include <stdio.h>",
|
||||
...declarations,
|
||||
"int main(void) { return rea_page_0000(); }",
|
||||
"int main(void) { return rea_inventory_0000(); }",
|
||||
"",
|
||||
].join("\n");
|
||||
};
|
||||
|
||||
@@ -56,23 +56,6 @@ export const documentationFactIssues = async (root, catalog) => {
|
||||
requireText(issues, templatePath, template, "docs/product-catalog.json");
|
||||
|
||||
const english = await readFile(join(root, "README.md"), "utf8");
|
||||
const schemaVersion = (id) => {
|
||||
const schema = catalog.schemas.find((candidate) => candidate.id === id);
|
||||
if (schema === undefined) throw new Error(`Missing ${id} catalog schema`);
|
||||
return String(schema.version);
|
||||
};
|
||||
requireText(
|
||||
issues,
|
||||
"README.md",
|
||||
english,
|
||||
`Evidence v${schemaVersion("evidence")}`,
|
||||
);
|
||||
requireText(
|
||||
issues,
|
||||
"README.md",
|
||||
english,
|
||||
`Process Capture v${schemaVersion("process_capture")}`,
|
||||
);
|
||||
requireText(issues, "README.md", english, "docs/product-catalog.json");
|
||||
return issues;
|
||||
};
|
||||
|
||||
@@ -17,12 +17,11 @@ const completionClaim = ({
|
||||
...new Set(targets.map(({ sha256: digest }) => digest)),
|
||||
].sort();
|
||||
const evidence = createEvidence(targets[0], COMPLETION_PROVIDER, {
|
||||
predicateType: "rea.verification/v1",
|
||||
predicateType: "rea.verification",
|
||||
operation: "verify_managed_conformance",
|
||||
parameters: {
|
||||
scenario_id: scenarioId,
|
||||
scenario_version: 1,
|
||||
result_schema_version: 1,
|
||||
artifact_sha256s: artifactSha256s,
|
||||
},
|
||||
result: { status },
|
||||
@@ -37,7 +36,6 @@ const completionClaim = ({
|
||||
id: COMPLETION_PROVIDER.id,
|
||||
version: COMPLETION_PROVIDER.version,
|
||||
},
|
||||
result_schema_version: 1,
|
||||
status,
|
||||
evidence_ids: [evidence.evidence_id],
|
||||
};
|
||||
@@ -151,7 +149,6 @@ const optionalClaims = ({ operatorManifest, ilspyOracle }) => [
|
||||
|
||||
/** Project completed managed assertions into the standard verifier report. */
|
||||
export const createManagedCompletionReport = (input, verifierRun) => ({
|
||||
schema_version: 1,
|
||||
verifier: {
|
||||
id: COMPLETION_PROVIDER.id,
|
||||
version: COMPLETION_PROVIDER.version,
|
||||
|
||||
@@ -41,14 +41,6 @@ export const nativeBoundaryLimits = {
|
||||
maxHeapItemBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
export const applicationGraphLimits = {
|
||||
max_types: 100,
|
||||
max_methods: 100,
|
||||
max_fields: 100,
|
||||
max_pinvoke_imports: 100,
|
||||
max_native_implementations: 100,
|
||||
};
|
||||
|
||||
export const comparisonLimits = {
|
||||
max_method_matches: 100,
|
||||
max_field_matches: 100,
|
||||
@@ -60,13 +52,6 @@ export const defaultIlBody = Buffer.from([
|
||||
]);
|
||||
|
||||
export function functionDossier(name) {
|
||||
const emptyPage = {
|
||||
items: [],
|
||||
total: 0,
|
||||
returned: 0,
|
||||
truncated: false,
|
||||
next_offset: null,
|
||||
};
|
||||
return {
|
||||
procedure: {
|
||||
address: "0x401000",
|
||||
@@ -80,23 +65,16 @@ export function functionDossier(name) {
|
||||
signature: null,
|
||||
locals: [],
|
||||
},
|
||||
pseudocode: {
|
||||
text: "",
|
||||
total_chars: 0,
|
||||
returned_chars: 0,
|
||||
truncated: false,
|
||||
next_offset: null,
|
||||
},
|
||||
assembly: emptyPage,
|
||||
comments: emptyPage,
|
||||
callers: emptyPage,
|
||||
callees: emptyPage,
|
||||
incoming_references: emptyPage,
|
||||
outgoing_references: emptyPage,
|
||||
referenced_strings: emptyPage,
|
||||
referenced_names: emptyPage,
|
||||
basic_blocks: emptyPage,
|
||||
instruction_scan: { scanned: 0, truncated: false },
|
||||
pseudocode: "",
|
||||
assembly: [],
|
||||
comments: [],
|
||||
callers: [],
|
||||
callees: [],
|
||||
incoming_references: [],
|
||||
outgoing_references: [],
|
||||
referenced_strings: [],
|
||||
referenced_names: [],
|
||||
basic_blocks: [],
|
||||
limitations: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
export function parseManagedAppManifest(rawManifest) {
|
||||
const manifest = object(rawManifest, "manifest");
|
||||
ensure(
|
||||
manifest.schema_version === 1,
|
||||
"manifest.schema_version must be exactly 1",
|
||||
);
|
||||
const target = object(manifest.target, "manifest.target");
|
||||
const methods = array(manifest.methods, "manifest.methods");
|
||||
ensure(manifest.methods.length > 0, "manifest.methods must not be empty");
|
||||
return {
|
||||
schema_version: 1,
|
||||
label: optionalString(manifest.label, "manifest.label"),
|
||||
target: {
|
||||
path: string(target.path, "manifest.target.path"),
|
||||
@@ -105,7 +100,7 @@ function parseManagedAppManifestMethod(rawMethod, index) {
|
||||
method.signature_sha256,
|
||||
`${prefix}.signature_sha256`,
|
||||
),
|
||||
il_size: ilSize(method, prefix),
|
||||
il_size: nonNegativeInteger(method.il_size, `${prefix}.il_size`),
|
||||
il_sha256:
|
||||
method.il_sha256 === undefined
|
||||
? undefined
|
||||
@@ -117,26 +112,6 @@ function parseManagedAppManifestMethod(rawMethod, index) {
|
||||
};
|
||||
}
|
||||
|
||||
function ilSize(method, prefix) {
|
||||
const hasIlSize = method.il_size !== undefined;
|
||||
const hasIlLength = method.il_length !== undefined;
|
||||
ensure(
|
||||
hasIlSize || hasIlLength,
|
||||
`${prefix}.il_size is required; il_length is accepted as a legacy alias`,
|
||||
);
|
||||
if (hasIlSize && hasIlLength)
|
||||
ensure(
|
||||
method.il_size === method.il_length,
|
||||
`${prefix}.il_size and ${prefix}.il_length disagree`,
|
||||
);
|
||||
const value = hasIlSize ? method.il_size : method.il_length;
|
||||
ensure(
|
||||
Number.isInteger(value) && value >= 0,
|
||||
`${prefix}.il_size must be a non-negative integer`,
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
function object(value, name) {
|
||||
ensure(
|
||||
value !== null && typeof value === "object" && !Array.isArray(value),
|
||||
@@ -163,6 +138,14 @@ function optionalString(value, name) {
|
||||
return string(value, name);
|
||||
}
|
||||
|
||||
function nonNegativeInteger(value, name) {
|
||||
ensure(
|
||||
Number.isInteger(value) && value >= 0,
|
||||
`${name} must be a non-negative integer`,
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
function digest(value, name) {
|
||||
const text = string(value, name);
|
||||
ensure(/^[a-f0-9]{64}$/u.test(text), `${name} must be a lowercase sha256`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { parseBinaryTarget } from "../../dist/domain/binaryTarget.js";
|
||||
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../../dist/application/ManagedApplicationGraphService.js";
|
||||
@@ -202,13 +202,6 @@ function verifyManagedAppManifestApplicationGraph({
|
||||
case_sensitive: expectedTrace.case_sensitive,
|
||||
},
|
||||
direction: "incoming",
|
||||
limits: {
|
||||
max_seed_matches: 10,
|
||||
max_depth: 4,
|
||||
max_nodes: 100,
|
||||
max_edges: 200,
|
||||
max_paths: 20,
|
||||
},
|
||||
});
|
||||
ensure(
|
||||
traced.ok,
|
||||
@@ -245,11 +238,8 @@ function verifyManagedAppManifestApplicationGraph({
|
||||
}
|
||||
|
||||
const projectManifestMethod = (state, token) => {
|
||||
const {
|
||||
appManifestInspectionLimits,
|
||||
appManifestMemberLimits,
|
||||
applicationGraphLimits,
|
||||
} = state.context;
|
||||
const { appManifestInspectionLimits, appManifestMemberLimits } =
|
||||
state.context;
|
||||
const existing = state.graphByToken.get(token);
|
||||
if (existing !== undefined) return existing;
|
||||
const method = state.inspectedMethods.get(token);
|
||||
@@ -289,7 +279,6 @@ const projectManifestMethod = (state, token) => {
|
||||
const projected = projectManagedApplicationGraphEvidence({
|
||||
managed_artifact: artifactEvidence,
|
||||
managed_members: memberEvidence,
|
||||
limits: applicationGraphLimits,
|
||||
});
|
||||
ensure(
|
||||
projected.ok,
|
||||
@@ -316,7 +305,7 @@ function inspectManagedMethodByToken(context, bytes, target, expectedMethod) {
|
||||
target,
|
||||
expectedMethod.token,
|
||||
);
|
||||
const method = members.methods.items.find(
|
||||
const method = members.methods.find(
|
||||
({ token }) => token === expectedMethod.token,
|
||||
);
|
||||
ensure(
|
||||
|
||||
@@ -39,11 +39,10 @@ async function runManagedAppManifestSelfTest(context) {
|
||||
sample.target,
|
||||
context.memberLimits,
|
||||
);
|
||||
const method = members.methods.items[0];
|
||||
const method = members.methods[0];
|
||||
assert.ok(method);
|
||||
const summary = await context.verifyManagedAppManifest(
|
||||
{
|
||||
schema_version: 1,
|
||||
label: "source-owned manifest verifier self-test",
|
||||
target: {
|
||||
path: sample.path,
|
||||
@@ -143,7 +142,7 @@ async function runOptionalIlspyOracle(context) {
|
||||
sample.target,
|
||||
context.memberLimits,
|
||||
);
|
||||
const method = members.methods.items[0];
|
||||
const method = members.methods[0];
|
||||
ensureIlspy(method !== undefined, "source-owned fixture has no method");
|
||||
const listOutput = await runIlspy(
|
||||
ilspyPath,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { parseBinaryTarget } from "../../dist/domain/binaryTarget.js";
|
||||
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
|
||||
import { buildManagedPeFixture } from "./managed-pe-fixture.mjs";
|
||||
import { createManagedConformanceOracleSupport } from "./managed-conformance-oracles.mjs";
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
@@ -19,35 +19,29 @@ export const requireMcpResult = (result, operation) => {
|
||||
return value.result;
|
||||
};
|
||||
|
||||
/** Verify direct-provider provenance through the complete Evidence resource. */
|
||||
export const requireEvidenceProvider = async (
|
||||
client,
|
||||
result,
|
||||
operation,
|
||||
providerId,
|
||||
) => {
|
||||
const evidence = await readMcpEvidence(client, result, operation);
|
||||
/** Verify provider identity directly from the tool result. */
|
||||
export const requireEvidenceProvider = (result, operation, providerId) => {
|
||||
const evidence = jsonValue(result);
|
||||
if (
|
||||
evidence?.provider?.id !== providerId ||
|
||||
evidence?.analysis_profile?.provider?.id !== providerId ||
|
||||
typeof evidence.analysis_profile.provider.version !== "string"
|
||||
evidence?.evidence?.provider?.id !== providerId ||
|
||||
evidence?.evidence?.analysis_profile?.provider?.id !== providerId ||
|
||||
typeof evidence.evidence.analysis_profile.provider.version !== "string"
|
||||
) {
|
||||
throw new Error(`${operation} omitted its concrete provider provenance`);
|
||||
}
|
||||
};
|
||||
|
||||
/** Verify composed-workflow and upstream provenance through full Evidence. */
|
||||
export const requireWorkflowEvidenceProvider = async (
|
||||
client,
|
||||
/** Verify composed workflow and upstream provider provenance inline. */
|
||||
export const requireWorkflowEvidenceProvider = (
|
||||
result,
|
||||
operation,
|
||||
expected,
|
||||
) => {
|
||||
const evidence = await readMcpEvidence(client, result, operation);
|
||||
const profile = evidence?.analysis_profile;
|
||||
const evidence = jsonValue(result);
|
||||
const profile = evidence?.evidence?.analysis_profile;
|
||||
const upstream = profile?.parameters?.upstream_analysis_profile;
|
||||
if (
|
||||
evidence?.provider?.id !== expected.workflowProviderId ||
|
||||
evidence?.evidence?.provider?.id !== expected.workflowProviderId ||
|
||||
profile?.provider?.id !== expected.workflowProviderId ||
|
||||
upstream?.provider?.id !== expected.upstreamProviderId ||
|
||||
typeof upstream.provider.version !== "string"
|
||||
@@ -57,35 +51,3 @@ export const requireWorkflowEvidenceProvider = async (
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const readMcpEvidence = async (client, result, operation) => {
|
||||
const compact = jsonValue(result);
|
||||
if (
|
||||
compact === null ||
|
||||
typeof compact !== "object" ||
|
||||
typeof compact.evidence_id !== "string" ||
|
||||
compact.evidence_uri !== `rea://evidence/${compact.evidence_id}`
|
||||
) {
|
||||
throw new Error(`${operation} omitted its Evidence reference`);
|
||||
}
|
||||
const resource = await client.readResource({ uri: compact.evidence_uri });
|
||||
const text = resource.contents.find(
|
||||
(content) =>
|
||||
content.uri === compact.evidence_uri && typeof content.text === "string",
|
||||
)?.text;
|
||||
if (text === undefined)
|
||||
throw new Error(`${operation} Evidence resource omitted JSON text`);
|
||||
const evidence = JSON.parse(text);
|
||||
if (
|
||||
evidence === null ||
|
||||
typeof evidence !== "object" ||
|
||||
evidence.evidence_id !== compact.evidence_id ||
|
||||
JSON.stringify(evidence.normalized_result) !==
|
||||
JSON.stringify(compact.result)
|
||||
) {
|
||||
throw new Error(
|
||||
`${operation} Evidence resource did not match its projection`,
|
||||
);
|
||||
}
|
||||
return evidence;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { analysisProfileSchema } from "../../dist/domain/analysisProfile.js";
|
||||
import { nativeValueTraceSchema } from "../../dist/domain/nativeValueTrace.js";
|
||||
|
||||
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const environment = () => ({
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
...(process.env.GHIDRA_INSTALL_DIR === undefined
|
||||
? {}
|
||||
: { GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR }),
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
});
|
||||
|
||||
const verifyGraph = (input, target, procedure, globalAddress) => {
|
||||
const graph = nativeValueTraceSchema.parse(input);
|
||||
assert.equal(graph.target_sha256, target.sha256);
|
||||
assert.equal(graph.seed, procedure);
|
||||
const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
|
||||
for (const kind of ["argument-binding", "parameter-use", "return-binding"]) {
|
||||
const edges = graph.edges.filter((edge) => edge.kind === kind);
|
||||
assert.ok(edges.length > 0, `Real native dependency graph lacks ${kind}`);
|
||||
for (const edge of edges) {
|
||||
const source = nodes.get(edge.source);
|
||||
const destination = nodes.get(edge.target);
|
||||
assert.ok(source, `Missing source node ${edge.source}`);
|
||||
assert.ok(destination, `Missing target node ${edge.target}`);
|
||||
assert.equal(edge.status, "derived");
|
||||
if (kind === "argument-binding") {
|
||||
assert.match(source.operation?.opcode ?? "", /^CALL(?:IND)?$/u);
|
||||
assert.equal(destination.kind, "parameter");
|
||||
assert.equal(edge.input_index, destination.parameter.ordinal + 1);
|
||||
} else if (kind === "parameter-use") {
|
||||
assert.equal(source.kind, "parameter");
|
||||
assert.ok(destination.operation.inputs[edge.input_index]);
|
||||
} else {
|
||||
assert.equal(source.operation?.opcode, "RETURN");
|
||||
assert.match(destination.operation?.opcode ?? "", /^CALL(?:IND)?$/u);
|
||||
assert.equal(edge.input_index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(
|
||||
graph.nodes.some((node) =>
|
||||
node.operation?.inputs.some((value) => value.location === globalAddress),
|
||||
),
|
||||
"Source-owned global operand is absent from the real dependency graph",
|
||||
);
|
||||
for (const node of graph.nodes)
|
||||
assert.match(node.evidence_id, /^ev_[a-f0-9]{64}$/u);
|
||||
for (const procedure of graph.procedures) {
|
||||
assert.equal(procedure.provider, "ghidra");
|
||||
assert.match(procedure.analysis_profile_digest, /^[a-f0-9]{64}$/u);
|
||||
}
|
||||
return graph;
|
||||
};
|
||||
|
||||
/** Verify real Ghidra dependency composition through production CLI and stdio MCP. */
|
||||
export async function verifyNativeValueE2e(target, procedure, globalAddress) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"trace-native-values",
|
||||
target.path,
|
||||
procedure,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--max-depth",
|
||||
"2",
|
||||
"--max-functions",
|
||||
"8",
|
||||
"--limit",
|
||||
"5000",
|
||||
"--json",
|
||||
],
|
||||
{ env: environment(), timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(evidence.operation, "trace_native_values");
|
||||
assert.equal(evidence.provider.id, "rea-workflow");
|
||||
const workflowProfile = analysisProfileSchema.parse(
|
||||
evidence.analysis_profile,
|
||||
);
|
||||
assert.equal(workflowProfile.provider.id, "rea-workflow");
|
||||
const upstreamProfile = analysisProfileSchema.parse(
|
||||
workflowProfile.parameters.upstream_analysis_profile,
|
||||
);
|
||||
assert.equal(upstreamProfile.provider.id, "ghidra");
|
||||
const cliGraph = verifyGraph(
|
||||
evidence.normalized_result,
|
||||
target,
|
||||
procedure,
|
||||
globalAddress,
|
||||
);
|
||||
for (const procedure of cliGraph.procedures)
|
||||
assert.equal(procedure.analysis_profile_digest, upstreamProfile.digest);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env: environment(),
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "native-values-real-e2e", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const opened = await client.callTool(
|
||||
{
|
||||
name: "open_binary",
|
||||
arguments: { path: target.path, provider: "ghidra" },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(opened.isError, true, JSON.stringify(opened));
|
||||
const inventory = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
assert.notEqual(inventory.isError, true, JSON.stringify(inventory));
|
||||
assert.ok(
|
||||
inventory.structuredContent?.result?.tool_availability.some(
|
||||
(tool) => tool.name === "trace_native_values" && tool.available,
|
||||
),
|
||||
"Native value tracing is unavailable in real provider discovery",
|
||||
);
|
||||
const result = await client.callTool(
|
||||
{
|
||||
name: "trace_native_values",
|
||||
arguments: { procedure, max_depth: 2, max_functions: 8, limit: 5000 },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
const mcpEvidence = parseEvidence({
|
||||
...result.structuredContent?.evidence,
|
||||
normalized_result: result.structuredContent?.result,
|
||||
});
|
||||
assert.equal(mcpEvidence.operation, "trace_native_values");
|
||||
assert.equal(mcpEvidence.provider.id, "rea-workflow");
|
||||
assert.deepEqual(mcpEvidence.analysis_profile, workflowProfile);
|
||||
const mcpGraph = verifyGraph(
|
||||
result.structuredContent?.result,
|
||||
target,
|
||||
procedure,
|
||||
globalAddress,
|
||||
);
|
||||
assert.deepEqual(mcpEvidence.normalized_result, mcpGraph);
|
||||
assert.deepEqual(mcpGraph, cliGraph);
|
||||
const closed = await client.callTool({
|
||||
name: "close_binary",
|
||||
arguments: {},
|
||||
});
|
||||
assert.notEqual(closed.isError, true, JSON.stringify(closed));
|
||||
const status = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
assert.equal(status.structuredContent?.result?.open, false);
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
return {
|
||||
mocked: false,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
nodes: cliGraph.total_nodes,
|
||||
edges: cliGraph.total_edges,
|
||||
decompilations: cliGraph.decompilations,
|
||||
unknowns: cliGraph.unknowns.length,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
export interface ProductCatalog {
|
||||
readonly catalog_schema_version: number;
|
||||
readonly package: {
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
@@ -46,6 +45,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 {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { format } from "prettier";
|
||||
|
||||
import { assertSameNames, loadSources } from "./catalog-core.mjs";
|
||||
import { assertSameNames, digest, loadSources } from "./catalog-core.mjs";
|
||||
import {
|
||||
createCliInventory,
|
||||
cliCommandDescriptionIssues,
|
||||
cliCommandOptionNames,
|
||||
} from "./catalog-cli.mjs";
|
||||
import { providerCatalog, toolFamilyCatalog } from "./catalog-builders.mjs";
|
||||
import { schemaCatalog } from "./catalog-schemas.mjs";
|
||||
|
||||
export {
|
||||
createCliInventory,
|
||||
@@ -15,6 +14,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,9 +27,9 @@ 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,
|
||||
package: {
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
@@ -39,7 +41,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,
|
||||
@@ -48,7 +50,6 @@ export const createProductCatalog = async (root) => {
|
||||
configuration: format === "unsupported" ? "detect-only" : "managed",
|
||||
}),
|
||||
),
|
||||
schemas: schemaCatalog(sources),
|
||||
cli: {
|
||||
primary_count: cli.primary.length,
|
||||
commands: cli.primary,
|
||||
@@ -56,7 +57,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),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
interface ToolClient {
|
||||
callTool(request: unknown, options: unknown): Promise<unknown>;
|
||||
}
|
||||
|
||||
interface SearchSummary {
|
||||
readonly count: number;
|
||||
readonly calls: 1;
|
||||
}
|
||||
|
||||
export function openAndVerifyLargeFixture(input: {
|
||||
readonly client: ToolClient;
|
||||
readonly options: unknown;
|
||||
readonly normalizedResult: (value: unknown, operation: string) => unknown;
|
||||
readonly path: string;
|
||||
readonly expectedCount: number;
|
||||
readonly symbolPrefix: string;
|
||||
readonly stringPrefix: string;
|
||||
}): Promise<{
|
||||
readonly procedures: SearchSummary;
|
||||
readonly strings: SearchSummary;
|
||||
}>;
|
||||
@@ -0,0 +1,83 @@
|
||||
/** Open the large fixture and verify complete inline search results. */
|
||||
export async function openAndVerifyLargeFixture({
|
||||
client,
|
||||
options,
|
||||
normalizedResult,
|
||||
path,
|
||||
expectedCount,
|
||||
symbolPrefix,
|
||||
stringPrefix,
|
||||
}) {
|
||||
const opened = await client.callTool(
|
||||
{ name: "open_binary", arguments: { path } },
|
||||
options,
|
||||
);
|
||||
if (opened.isError === true)
|
||||
throw new Error("Hopper rejected the large fixture");
|
||||
const procedures = await collectAllMatches({
|
||||
client,
|
||||
options,
|
||||
normalizedResult,
|
||||
tool: "search_procedures",
|
||||
pattern: symbolPrefix.replace(/^_+/u, ""),
|
||||
expectedPrefix: symbolPrefix.replace(/^_+/u, ""),
|
||||
expectedCount,
|
||||
normalize: (value) => value.replace(/^_+/u, ""),
|
||||
});
|
||||
const strings = await collectAllMatches({
|
||||
client,
|
||||
options,
|
||||
normalizedResult,
|
||||
tool: "search_strings",
|
||||
pattern: stringPrefix,
|
||||
expectedPrefix: stringPrefix,
|
||||
expectedCount,
|
||||
normalize: (value) => value,
|
||||
});
|
||||
return { procedures, strings };
|
||||
}
|
||||
|
||||
async function collectAllMatches({
|
||||
client,
|
||||
options,
|
||||
normalizedResult,
|
||||
tool,
|
||||
pattern,
|
||||
expectedPrefix,
|
||||
expectedCount,
|
||||
normalize,
|
||||
}) {
|
||||
const matches = normalizedResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: tool,
|
||||
arguments: { pattern, mode: "literal", case_sensitive: true },
|
||||
},
|
||||
options,
|
||||
),
|
||||
tool,
|
||||
);
|
||||
if (!Array.isArray(matches) || matches.length !== expectedCount)
|
||||
throw new Error(`${tool} did not return every expected match`);
|
||||
const addresses = new Set();
|
||||
const values = new Set();
|
||||
for (const item of matches) {
|
||||
const value = normalize(item.value);
|
||||
if (
|
||||
typeof item.address !== "string" ||
|
||||
!/^0x[0-9a-f]+$/iu.test(item.address)
|
||||
)
|
||||
throw new Error(`${tool} returned a malformed address`);
|
||||
if (addresses.has(item.address) || values.has(value))
|
||||
throw new Error(`${tool} returned duplicate matches`);
|
||||
addresses.add(item.address);
|
||||
values.add(value);
|
||||
}
|
||||
const expectedValues = Array.from(
|
||||
{ length: expectedCount },
|
||||
(_, index) => `${expectedPrefix}${String(index).padStart(4, "0")}`,
|
||||
);
|
||||
if (expectedValues.some((value) => !values.has(value)))
|
||||
throw new Error(`${tool} omitted expected matches`);
|
||||
return { count: values.size, calls: 1 };
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
||||
export async function loadRealHopperFixtureTargets(manifestPath) {
|
||||
const canonicalManifest = await realpath(manifestPath);
|
||||
const manifest = JSON.parse(await readFile(canonicalManifest, "utf8"));
|
||||
if (manifest?.schemaVersion !== 1 || !Array.isArray(manifest.fixtures))
|
||||
if (!Array.isArray(manifest?.fixtures))
|
||||
throw new Error("Invalid conformance fixture manifest");
|
||||
const primary = fixtureEntry(manifest.fixtures, "c");
|
||||
const secondary = fixtureEntry(manifest.fixtures, "version-v2");
|
||||
@@ -69,7 +69,7 @@ function requireLargeOracle(largeOracle) {
|
||||
largeOracle.symbolCount !== largeOracle.stringCount ||
|
||||
largeOracle.symbolCount <= 0
|
||||
)
|
||||
throw new Error("Large fixture omitted its exact pagination oracle");
|
||||
throw new Error("Large fixture omitted its complete-inventory oracle");
|
||||
return largeOracle;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,23 +31,21 @@ export const verifyHopperFunctionBasics = async (
|
||||
arguments: {
|
||||
procedure,
|
||||
direction: "outgoing",
|
||||
limit: 10,
|
||||
max_instructions: 100,
|
||||
},
|
||||
},
|
||||
options,
|
||||
),
|
||||
"procedure_references",
|
||||
);
|
||||
if (!Array.isArray(references?.references?.items)) {
|
||||
throw new Error("procedure_references returned an invalid bounded result");
|
||||
if (!Array.isArray(references?.references)) {
|
||||
throw new Error("procedure_references returned an invalid result");
|
||||
}
|
||||
|
||||
const instructions = requireMcpResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: "read_function_instructions",
|
||||
arguments: { procedure, limit: 2 },
|
||||
arguments: { procedure },
|
||||
},
|
||||
options,
|
||||
),
|
||||
@@ -55,17 +53,14 @@ export const verifyHopperFunctionBasics = async (
|
||||
);
|
||||
if (
|
||||
instructions?.procedure?.address !== procedure ||
|
||||
!Array.isArray(instructions.instructions?.items) ||
|
||||
instructions.instructions.items.length === 0 ||
|
||||
instructions.instructions.items.length > 2
|
||||
!Array.isArray(instructions.instructions) ||
|
||||
instructions.instructions.length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
"read_function_instructions returned an invalid bounded window",
|
||||
);
|
||||
throw new Error("read_function_instructions returned an invalid result");
|
||||
}
|
||||
|
||||
return {
|
||||
outgoingReferenceCount: references.references.items.length,
|
||||
instructionWindowCount: instructions.instructions.items.length,
|
||||
outgoingReferenceCount: references.references.length,
|
||||
instructionWindowCount: instructions.instructions.length,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
|
||||
import {
|
||||
requireMcpResult,
|
||||
requireEvidenceProvider,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Verify real Hopper inventories and address filters through the MCP transport. */
|
||||
export async function verifyHopperInventories(client, options) {
|
||||
const inventoryCounts = {};
|
||||
for (const operation of ["list_strings", "list_names"]) {
|
||||
const called = await client.callTool(
|
||||
{ name: operation, arguments: {} },
|
||||
options,
|
||||
);
|
||||
const inventory = requireMcpResult(called, operation);
|
||||
requireEvidenceProvider(called, operation, HOPPER_PROVIDER_IDENTITY.id);
|
||||
assert.ok(
|
||||
Array.isArray(inventory) && inventory.length > 0,
|
||||
`${operation} omitted the real fixture inventory`,
|
||||
);
|
||||
let previous = -1n;
|
||||
for (const item of inventory) {
|
||||
assert.match(item.address, /^0x[0-9a-f]+$/u);
|
||||
assert.equal(typeof item.value, "string");
|
||||
const address = BigInt(item.address);
|
||||
assert.ok(
|
||||
address > previous,
|
||||
`${operation} is not numerically address-sorted`,
|
||||
);
|
||||
previous = address;
|
||||
}
|
||||
const filtered = requireMcpResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: operation,
|
||||
arguments: { address: inventory[0].address },
|
||||
},
|
||||
options,
|
||||
),
|
||||
operation,
|
||||
);
|
||||
assert.deepEqual(filtered, [inventory[0]]);
|
||||
inventoryCounts[operation] = inventory.length;
|
||||
}
|
||||
return inventoryCounts;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
interface ToolClient {
|
||||
callTool(request: unknown, options: unknown): Promise<unknown>;
|
||||
}
|
||||
|
||||
interface PaginationSummary {
|
||||
readonly count: number;
|
||||
readonly pages: number;
|
||||
}
|
||||
|
||||
export function verifyLargeFixturePagination(input: {
|
||||
readonly client: ToolClient;
|
||||
readonly options: unknown;
|
||||
readonly normalizedResult: (value: unknown, operation: string) => unknown;
|
||||
readonly expectedCount: number;
|
||||
readonly symbolPrefix: string;
|
||||
readonly stringPrefix: string;
|
||||
}): Promise<{
|
||||
readonly procedures: PaginationSummary;
|
||||
readonly strings: PaginationSummary;
|
||||
}>;
|
||||
|
||||
export function openAndVerifyLargeFixture(input: {
|
||||
readonly client: ToolClient;
|
||||
readonly options: unknown;
|
||||
readonly normalizedResult: (value: unknown, operation: string) => unknown;
|
||||
readonly path: string;
|
||||
readonly expectedCount: number;
|
||||
readonly symbolPrefix: string;
|
||||
readonly stringPrefix: string;
|
||||
}): Promise<{
|
||||
readonly procedures: PaginationSummary;
|
||||
readonly strings: PaginationSummary;
|
||||
}>;
|
||||
@@ -1,138 +0,0 @@
|
||||
const PAGE_LIMIT = 100;
|
||||
|
||||
/** Open the large fixture and exhaustively prove its public search pages. */
|
||||
export async function openAndVerifyLargeFixture({
|
||||
client,
|
||||
options,
|
||||
normalizedResult,
|
||||
path,
|
||||
expectedCount,
|
||||
symbolPrefix,
|
||||
stringPrefix,
|
||||
}) {
|
||||
const opened = await client.callTool(
|
||||
{ name: "open_binary", arguments: { path } },
|
||||
options,
|
||||
);
|
||||
if (opened.isError === true)
|
||||
throw new Error("Hopper rejected the large fixture");
|
||||
return verifyLargeFixturePagination({
|
||||
client,
|
||||
options,
|
||||
normalizedResult,
|
||||
expectedCount,
|
||||
symbolPrefix,
|
||||
stringPrefix,
|
||||
});
|
||||
}
|
||||
|
||||
/** Exhaustively prove the source-owned large fixture through public search pages. */
|
||||
export async function verifyLargeFixturePagination({
|
||||
client,
|
||||
options,
|
||||
normalizedResult,
|
||||
expectedCount,
|
||||
symbolPrefix,
|
||||
stringPrefix,
|
||||
}) {
|
||||
const procedures = await collectExactPages({
|
||||
client,
|
||||
options,
|
||||
normalizedResult,
|
||||
tool: "search_procedures",
|
||||
pattern: symbolPrefix.replace(/^_+/u, ""),
|
||||
expectedPrefix: symbolPrefix.replace(/^_+/u, ""),
|
||||
expectedCount,
|
||||
normalize: (value) => value.replace(/^_+/u, ""),
|
||||
});
|
||||
const strings = await collectExactPages({
|
||||
client,
|
||||
options,
|
||||
normalizedResult,
|
||||
tool: "search_strings",
|
||||
pattern: stringPrefix,
|
||||
expectedPrefix: stringPrefix,
|
||||
expectedCount,
|
||||
normalize: (value) => value,
|
||||
});
|
||||
return { procedures, strings };
|
||||
}
|
||||
|
||||
async function collectExactPages({
|
||||
client,
|
||||
options,
|
||||
normalizedResult,
|
||||
tool,
|
||||
pattern,
|
||||
expectedPrefix,
|
||||
expectedCount,
|
||||
normalize,
|
||||
}) {
|
||||
const seenAddresses = new Set();
|
||||
const seenValues = new Set();
|
||||
let offset = 0;
|
||||
let pages = 0;
|
||||
while (true) {
|
||||
const page = normalizedResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: tool,
|
||||
arguments: {
|
||||
pattern,
|
||||
mode: "literal",
|
||||
case_sensitive: true,
|
||||
offset,
|
||||
limit: PAGE_LIMIT,
|
||||
},
|
||||
},
|
||||
options,
|
||||
),
|
||||
`${tool} offset ${offset}`,
|
||||
);
|
||||
validatePage(page, { offset, expectedCount });
|
||||
for (const item of page.items) {
|
||||
const value = normalize(item.value);
|
||||
if (item.value_truncated || !/^0x[0-9a-f]+$/iu.test(item.address))
|
||||
throw new Error(`${tool} returned truncated or malformed evidence`);
|
||||
if (seenAddresses.has(item.address) || seenValues.has(value))
|
||||
throw new Error(`${tool} returned duplicate evidence`);
|
||||
seenAddresses.add(item.address);
|
||||
seenValues.add(value);
|
||||
}
|
||||
pages += 1;
|
||||
if (page.next_offset === null) break;
|
||||
offset = page.next_offset;
|
||||
}
|
||||
const expectedValues = Array.from(
|
||||
{ length: expectedCount },
|
||||
(_, index) => `${expectedPrefix}${String(index).padStart(4, "0")}`,
|
||||
);
|
||||
if (
|
||||
seenValues.size !== expectedCount ||
|
||||
expectedValues.some((value) => !seenValues.has(value))
|
||||
)
|
||||
throw new Error(`${tool} did not exhaustively prove the fixture values`);
|
||||
return { count: seenValues.size, pages };
|
||||
}
|
||||
|
||||
function validatePage(page, { offset, expectedCount }) {
|
||||
if (
|
||||
page === null ||
|
||||
typeof page !== "object" ||
|
||||
!Array.isArray(page.items) ||
|
||||
page.offset !== offset ||
|
||||
page.limit !== PAGE_LIMIT ||
|
||||
page.total !== expectedCount ||
|
||||
page.items.length !== Math.min(PAGE_LIMIT, expectedCount - offset)
|
||||
)
|
||||
throw new Error("Hopper returned an inconsistent fixture search page");
|
||||
const expectedNext =
|
||||
offset + page.items.length < expectedCount
|
||||
? offset + page.items.length
|
||||
: null;
|
||||
if (
|
||||
page.next_offset !== expectedNext ||
|
||||
page.has_more !== (expectedNext !== null)
|
||||
)
|
||||
throw new Error("Hopper returned inconsistent pagination metadata");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user