- Status: Accepted
- Date: 2026-07-15
- Implementation status: The provider registry, deterministic selection, target binding, analysis-profile commitment, and snapshot/Evidence migration are implemented. Ghidra discovery, target/profile resolution, doctor checks, the private headless-session foundation, ten read-only inventory capabilities, and nine function-analysis capabilities are implemented with real cross-format conformance.
Before this decision, REA composed the artifact, native macOS, and Hopper
providers with CompositeProvider. That class intentionally requires every
operation to have one route. Hopper and Ghidra will both implement the deep
static-analysis operation family, so putting both into that composite would
either fail at construction or make array order an undeclared selection policy.
The active BinarySession also owned one composite client. open_binary had no
provider selector, and the domain BinaryTarget contained Hopper loader
arguments. Analysis snapshot v1 committed those loader arguments and each
cached entry's provider identity, but it did not commit the selected analysis
engine, engine version, or complete analysis configuration. Two analyses of the
same bytes could therefore disagree because of language, compiler, loader,
analyzer, or provider-version differences while appearing target-compatible.
These constraints are embodied in the current
CompositeProvider,
runtime composition,
BinarySession,
BinaryTarget,
analysis snapshot, and
session lifecycle inputs.
The next deep provider must not weaken the existing product invariants:
- stable provider-neutral CLI and MCP tool names;
- exact provenance and explicit unavailable or degraded capabilities;
- CLI and MCP parity;
- no silent provider fallback;
- provider-specific code outside the domain and application models;
- local, actionable diagnostics with only genuine secrets redacted; and
- snapshot reuse only when semantic analysis inputs are identical.
- Hopper and Ghidra need to declare overlapping operations without ambiguous dispatch.
- A target must have one explainable deep-analysis producer for its lifetime.
- Target-free discovery must remain useful and must not start an analysis process.
- Existing Hopper-only configurations must keep working when no selector is supplied.
- Adding a second installed provider must not silently change results.
- Snapshot and Evidence compatibility must include every setting that can materially change analysis.
- Provider-specific open settings must not leak into
BinaryTargetor become generic application fields.
- Deep provider: an engine such as Hopper or Ghidra that implements the overlapping static-analysis operation family.
- Auxiliary provider: a provider with a disjoint operation family, such as artifact inventory or native macOS inspection.
- Candidate: a registered deep provider before target selection.
- Binding: the selected deep provider and analysis profile attached to one active target.
- Analysis profile: a canonical commitment to the selected provider, provider version, and normalized semantic analysis settings.
- Available: the adapter and its required local runtime can be used on this host.
- Supported: the candidate accepts the parsed target and supplied open options. Availability and target support are separate observations.
CompositeProvider remains the mechanism for operation families whose routes
are disjoint. It must continue rejecting duplicate operation declarations. It
must not acquire priority, racing, fallback, or provider-selection behavior.
A new AnalysisProviderRegistry owns the configured deep-provider candidates.
Provider IDs are unique; duplicate IDs are a configuration error. The registry
may hold multiple candidates that declare the same operation. It performs
candidate discovery, target-support evaluation, option parsing, profile
resolution, and selection, but it does not choose a different provider for each
request.
After selection, the session composes exactly one bound deep provider with the auxiliary disjoint families:
target-free discovery
|
AnalysisProviderRegistry
/ \
Hopper Ghidra
\ /
one selected binding
|
+-------------------------+-------------------------+
| | |
selected deep family artifact family native family
+-------------------------+-------------------------+
|
BinarySession
An auxiliary operation continues to report its actual auxiliary provider as provenance. A deep operation always reports the provider in the active binding. The synthetic composite identity is never evidence that two engines jointly produced one observation.
Operation ownership is fixed by family:
| Operation family | Executor |
|---|---|
| Deep direct operations and provider-native function analysis | The one bound Hopper or Ghidra provider |
| Application-enhanced deep workflows | Application composition over the bound provider's declared operations |
| Native macOS inspection | The disjoint native provider |
| Artifact inventory and extraction | The disjoint artifact provider |
| Browser and Electron observation | Their independently authorized CDP providers; they do not participate in deep-provider selection |
| Session lifecycle, Evidence, comparisons, and reconstruction | The application/session layer using the exact provider provenance recorded by their source operations |
Selecting a deep provider selects the whole deep family. If that provider marks an operation unavailable, REA returns the unavailability; it does not borrow that one operation from another deep provider. Application-enhanced workflows run only when the bound provider supplies their declared dependencies.
Each registered candidate exposes three bounded adapter-owned observations:
- identity and declared capability descriptors;
- host availability, including an actionable rejection when unavailable; and
- target support plus normalized profile resolution when a target is known.
Target-free discovery may inspect configuration, manifests, executable metadata, and documented version files. It must not import a target, create a project, open a GUI, or start the long-lived analysis process. Any short-lived version probe added later must be explicitly bounded and truthfully declared as a process effect.
Candidate output is sorted by provider ID for determinism. Sort order is not selection priority. Availability and support use stable codes plus a human diagnostic:
| Code | Meaning |
|---|---|
not_configured |
Required provider location or configuration is absent |
executable_missing |
The configured provider entry point is missing or not runnable |
runtime_missing |
A required provider runtime, such as Java, is unavailable |
unsupported_host |
The host platform or architecture is unsupported |
unsupported_version |
The discovered provider or runtime version is unsupported |
target_kind_unsupported |
The provider does not accept this target kind |
target_format_unsupported |
The provider does not accept this target format |
architecture_unsupported |
The selected target architecture is unsupported |
target_role_unsupported |
The provider rejects this executable image role |
managed_target_unsupported |
The provider does not admit managed code through this deep path |
open_options_invalid |
The selected adapter rejected its open options |
version_unresolved |
A concrete analysis-engine version could not be committed |
Diagnostics retain useful local paths, observed versions, architectures, and mismatch details. Credentials, authorization headers, license secrets, and other genuine secrets remain redacted.
The caller-visible selectors are:
- MCP
open_binary.provider_id; rea ... --provider <provider-id|auto>on every one-shot command that needs deep analysis; andREA_ANALYSIS_PROVIDER=<provider-id|auto>, defaulting toauto.
The literal value auto is reserved and cannot be a provider ID. Precedence is
request or CLI flag, then environment configuration, then the default auto.
An explicit request for auto overrides a concrete environment preference and
re-enters automatic selection.
Selection occurs after provider-neutral target parsing and before a deep client is created:
- A concrete requested provider ID must exist, be available, support the target, accept its options, and resolve a concrete version and profile. Any failure returns a typed selection error; another provider is never tried.
autofilters candidates by availability and target support.- Exactly one usable candidate is selected with source
auto-single-candidate. - More than one usable candidate is an
ambiguousselection error. The diagnostic lists the candidates and asks forprovider_id,--provider, orREA_ANALYSIS_PROVIDER. REA does not prefer Hopper, Ghidra, registration order, the fastest startup, or the first health response. - When no deep candidate is usable, an automatic open may still establish an unbound target so disjoint artifact or native operations remain available. Deep operations are then explicitly unavailable with the candidate rejections. A concrete request, or a concrete environment preference for a deep-eligible target, remains a hard requirement and fails instead of becoming unbound.
- A target that no deep adapter declares support for, such as an artifact-only
target, is
not_applicablerather than a failed deep analysis. Supplying a concrete provider in that open request is still an actionable unsupported- target error. A global environment preference does not make an artifact-only target require a deep provider.
This preserves target opening for non-Hopper capabilities on hosts without a deep engine while ensuring that installing a second usable deep engine never changes analysis silently.
An active deep binding contains:
canonical target identity
selected provider identity and concrete version
selection source: request | environment | auto-single-candidate
analysis profile commitment
owned provider client, once started
The binding is immutable until close_binary or an explicit open/switch
transaction. Provider client creation may remain lazy, but laziness cannot
change which provider was selected.
- Opening the same canonical target without a new selector keeps the existing binding.
- Opening the same target with the same provider and profile is idempotent.
- A different explicit provider or a profile-changing option is a real switch: drain active calls, close owned resources, resolve the new binding, and start it as one serialized transaction.
- If a switch fails, restore the prior target and binding on a best-effort transactional path and return the original switch error.
- Provider process exit, timeout, cancellation, capability failure, or adapter error never triggers fallback. The binding stays visible and unavailable until the caller explicitly switches or closes it.
- A snapshot never chooses or changes the provider. Selection happens first; snapshot compatibility is checked against the resulting binding.
Calls already admitted to one binding complete or cancel against that binding. No request can cross a concurrent target or provider transition.
BinaryTarget retains only provider-neutral source identity and classification:
canonical path, source digest, generic kind and format, selected architecture,
and available architectures. Hopper loader arguments leave the domain model.
Provider-owned databases use a generic analysis-database classification; their
adapter determines support. A legacy .hop target is therefore accepted only
by Hopper without teaching the domain how to open it. Ghidra v1 continues to use
private temporary projects and does not open user projects.
The generic transport may carry one bounded JSON provider_options object.
Only the selected adapter parses its keys and reports open_options_invalid at
that boundary. If this object is exposed over MCP, the CLI parity surface is
--provider-options-json with the same JSON schema and limits. Unknown keys,
wrong-provider options, excessive depth or size, and non-JSON values fail
closed.
Environment settings that are inherently adapter-specific, such as the Hopper
launcher or future Ghidra installation path, remain in adapter configuration.
They do not become BinaryTarget fields. The application layer transports an
opaque, already-bounded JSON value and the generic commitment described below;
it does not interpret Hopper or Ghidra option names.
Before creating a deep client, the selected adapter normalizes every semantic input that can affect analysis. Examples include Hopper loader selection and architecture flags, or Ghidra language ID, compiler specification, import mode, analyzer set, and analyzer options. Defaults are made explicit, unordered sets are sorted, and implicit host-dependent choices are resolved.
The adapter returns this provider-neutral envelope:
{
"schema_version": 1,
"provider": { "id": "ghidra", "version": "<concrete version>" },
"provider_profile_schema_version": 1,
"parameters": {},
"digest": "<sha256 of RFC 8785 canonical JSON excluding digest>"
}parameters is bounded JSON interpreted only by that provider adapter. It
contains analysis semantics, not credentials, authorization data, temporary
paths, installation paths, process IDs, timestamps, or other incidental host
state. The generic domain validates the envelope and digest but treats the
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.
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 the provider imports the target.
Analysis snapshot v2 replaces Hopper loader arguments with an explicit binding commitment:
snapshot_version: 2
target: digest + generic format/kind + selected architecture
binding: provider identity + analysis profile commitment
entries: exact immutable queries
evidence_bundle: retained Evidence records
The snapshot target identity, canonical snapshot content digest, and every query ID commit the provider ID, concrete provider version, and profile digest. 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;
- operation and canonical parameters; and
- the existing cacheability and immutability rules.
Provider or profile switches select a separate cache partition and cannot reuse 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.
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.
Existing Evidence v2 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
replay, or support an "unchanged" conclusion that depends on identical analysis
semantics.
Different providers or profile digests are never snapshot-compatible. A derived comparison may compare cross-provider normalized facts only when its own contract explicitly defines that semantic comparison. Exact pseudocode, provider text, or provider-specific metadata is not treated as equivalent. A missing or incompatible profile yields unknown or incompatible, never equal.
binary_session, rea providers, and rea capabilities expose the same
candidate and binding model. The target-free form does not start providers and
uses unknown for target support:
{
"analysis_provider_binding": null,
"analysis_provider_candidates": [
{
"provider": {
"id": "hopper",
"name": "Hopper Disassembler",
"version": "<detected version>"
},
"availability": {
"status": "available",
"code": null,
"reason": null,
"diagnostics": {}
},
"target_support": {
"status": "unknown",
"code": null,
"reason": "No target is open."
},
"selected": false,
"capabilities": []
}
]
}With a binding, analysis_provider_binding includes the selected identity,
selection source, and complete profile commitment. Every candidate reports
supported, unsupported, or unknown target support and whether it was
selected. Candidate capability descriptors preserve supported, unsupported,
and degraded truth independently.
An open target also exposes analysis_run. Its run ID is allocated by the
session before client construction and propagated into selected Hopper or
Ghidra bridge handshake and process environment. Dynamic clients retain
provider-attributed lineage snapshots only after revalidating launcher
parent/command and every observed launcher or descendant run token. not_observed
means no dynamic provider has supplied a snapshot. Once one or more start,
snapshots preserves every provider observation; each remains explicitly
unavailable or verified and records its observed_at time. Retained
snapshots are not live inventories, and a point-in-time observation never proves
historical absence.
The existing flat provider, providers, and capabilities status fields stay
during the 1.x compatibility window. Their effective deep-operation entries
come only from the active binding; target-free ambiguity is unavailable rather
than arbitrarily collapsed. The new candidate fields are authoritative for
selection. A future major release may remove the synthetic composite provider
field after callers have migrated.
Selection failures extend the tagged ProviderSelectionError with a stable
reason:
unknown_provider;provider_unavailable;target_unsupported;ambiguous;invalid_options.
The error includes the requested ID or auto, candidate IDs, rejection codes,
and actionable local diagnostics. AnalysisCapabilityUnavailableError remains
the error for an operation the selected provider cannot supply.
ProviderAdapterError and more specific provider errors remain failures of the
already selected provider. None authorizes fallback.
A snapshot whose provider or profile differs from the selected binding returns
EvidenceIntegrityError with stable profile_mismatch details. It is an
integrity incompatibility after selection, not permission to select the
snapshot's provider.
open_binary.provider_idand one-shot--provideruse the same parser and selection service.provider_id: "auto",--provider auto, andREA_ANALYSIS_PROVIDER=autohave identical auto semantics.- Startup targets opened from environment configuration use the same service.
rea providers,rea capabilities, and target-freebinary_sessionuse the same candidate snapshot and reason codes.- Guided prompt
provider_idcontext is a preference to forward into the open operation; it does not create a hidden routing channel. - Tool names and normalized output contracts remain provider-neutral. REA does
not add
ghidra_decompileorhopper_decompiletools.
| Existing behavior or data | Decision |
|---|---|
| No provider selector, only Hopper usable | auto selects the sole candidate; behavior remains equivalent |
| No deep provider usable | Automatic open may remain unbound for auxiliary operations; deep capabilities explain why they are unavailable |
| Hopper and Ghidra both usable, no preference | Open fails with ambiguous; installation order never becomes policy |
| Existing Hopper environment settings | Remain supported as Hopper adapter configuration |
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 |
| 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 |
The deprecated loaderArgs response must never be read to select a provider or
validate a snapshot. It is empty for non-Hopper bindings and may be removed only
with a separately documented major contract change.
| Scenario | Result |
|---|---|
| Hopper is the only usable candidate, selector omitted | Hopper is bound by auto-single-candidate |
| Ghidra is the only usable candidate, selector omitted | Ghidra is bound by auto-single-candidate |
| Both are usable, selector omitted | Typed ambiguous error; neither provider starts |
| Environment selects Ghidra, request omits a selector | Ghidra is required and bound or the open fails with its rejection |
| Environment selects Ghidra, request selects Hopper | Request wins; Hopper is required |
Environment selects Ghidra, request selects auto |
Auto policy runs and is ambiguous when both are usable |
| Unknown concrete provider ID | Typed unknown_provider; no target transition |
| Selected provider rejects the target or options | Typed rejection from that candidate only |
| Selected provider times out after binding | Binding remains selected and unavailable; no fallback |
| 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 |
Positive consequences:
- Hopper and Ghidra can coexist without ambiguous operation routes.
- Provider choice is reproducible, visible, and stable for a target.
- Installation of another provider cannot silently change analysis.
- Snapshot and Evidence identities become analysis-configuration aware.
- Provider adapters retain ownership of their option schemas and diagnostics.
- Auxiliary capabilities remain usable when no deep engine is installed.
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.
- Providers must expose deterministic availability, support, version, and profile resolution before they can be admitted.
- Status and error contracts gain additive structured metadata.
- Hopper needs an adapter-owned version/profile implementation before profile- exact snapshot persistence is complete.
Array order is not a product policy, becomes fragile during wiring changes, and hides ambiguity from callers.
Machine load would determine provenance and results. It also starts resources the operator did not select and makes cancellation and cleanup harder.
Mid-session fallback can combine incompatible engine state and evidence under one apparent target. Explicit close or switch is required instead.
Parallel Hopper and Ghidra tool families would leak adapters into the product contract and prevent shared workflows from using capability metadata.
That repeats the current Hopper coupling and makes every generic consumer learn provider schemas. Only canonical profile commitments cross the adapter boundary.
Provider versions, analyzer defaults, loaders, languages, and compiler models can change results without changing either value.
The missing engine version and analysis settings cannot be reconstructed from Hopper loader arguments. A fabricated profile would turn unknown provenance into false certainty.
The first six implementation stages are shipped:
- Provider-specific target state was replaced by generic profile and snapshot v2 commitments while preserving Hopper execution behavior.
- The registry, selectors, candidate status, binding lifecycle, and overlapping-provider production seams were added.
- Proven process-lifecycle primitives were extracted for shared use.
- Ghidra availability, target/profile resolution, doctor checks, and a private read-only headless session were admitted without declaring operations.
- Program identity, procedures, strings, symbols, memory blocks, address/name resolution, containing-procedure resolution, and bounded search were admitted with exact wire schemas and real debug/stripped ELF conformance.
- Function metadata, persistent bounded 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.
Future stages may deepen format and indirect-flow coverage, but must continue to compare normalized semantics rather than provider-specific pseudocode or assembly text. Ghidra continues to omit GUI and mutation operations, so unsupported requests cannot route to an unverified implementation.