diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 242253783f..55221d1261 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,10 +16,9 @@ import inventory. For current provider wiring, start with
[`src/composition/binary.ts`](src/composition/binary.ts) for deep candidates and
the other `src/composition/` factories for tool services, including
[`gpu.ts`](src/composition/gpu.ts) and [`model.ts`](src/composition/model.ts).
-Live Frida and Cutter adapters are constructed directly in
-[`src/cli/fridaCommands.ts`](src/cli/fridaCommands.ts),
-[`src/cli/cutterCommands.ts`](src/cli/cutterCommands.ts), and
-[`src/server/createServer.ts`](src/server/createServer.ts).
+Live Frida, Cutter, GDB and Rizin construction also uses shared composition
+factories. For service ownership, availability and catalog declarations, follow
+the [capability composition guide](docs/capability-composition.md).
Production factories construct fresh providers without starting engines or
acquiring targets; optional adapter failures must preserve successful peers. Run
`npm run verify:test-discovery` after adding or moving tests. Resolved import
diff --git a/docs/architecture.mermaid b/docs/architecture.mermaid
index 4f36c28aac..0a54a27025 100644
--- a/docs/architecture.mermaid
+++ b/docs/architecture.mermaid
@@ -8,7 +8,7 @@ graph TD
end
subgraph "Production Composition"
- COMPOSITION["src/composition/
fresh typed provider and session factories
one-shot binding and independent optional loading
static WASM/EVM descriptors: named contracts, factories, retryable lifetimes"]
+ COMPOSITION["src/composition/
fresh typed provider and session factories
one-shot binding and independent optional loading
typed capability bindings: construction, availability and retryable lifetimes
pure capability declarations feed provider catalogs"]
end
subgraph "Adapters"
@@ -23,6 +23,7 @@ graph TD
SESSION["SessionProviderRouter + BinarySession
one immutable deep binding per target
disjoint auxiliary routing"]
REGISTRY["AnalysisProviderRegistry
sorted deep candidates
deterministic selection, no fallback
ProviderInspectionScope: prerequisite observation lifetime"]
RECORDS["src/application/investigation/
composed InvestigationRecords + narrow ports
EvidenceLedger and Unknown ownership"]
+ LIFETIMES["CapabilityScope
host-owned service close capabilities
released peers removed; failed cleanup retained for retry"]
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache
binary target/profile snapshots and session file ownership
record coordination and post-commit notifications"]
JS_RECON["src/application/javascript/ + src/artifacts/javascript/
JavaScript artifact reconstruction
directory/ASAR reads
AST-only bundle projection"]
end
@@ -99,6 +100,7 @@ graph TD
MAIN --> COMPOSITION
CLI --> COMPOSITION
SERVER --> COMPOSITION
+ COMPOSITION --> LIFETIMES
COMPOSITION --> SESSION
APP --> FIRMWARE
FIRMWARE --> PROCESS
@@ -149,8 +151,7 @@ graph TD
OBJDUMP --> DOMAIN
COMPOSITION --> RIZIN
RIZIN --> DOMAIN
- CLI --> GDB
- SERVER --> GDB
+ COMPOSITION --> GDB
GDB --> PROCESS
GDB --> DOMAIN
HOPPER --> HOPPER_BRIDGE
@@ -206,8 +207,7 @@ graph TD
NATIVE_READERS --> DOMAIN
COMPOSITION --> FORMAT_READERS
FORMAT_READERS --> DOMAIN
- SERVER --> RUNTIME
- CLI --> RUNTIME
+ COMPOSITION --> RUNTIME
RUNTIME --> DOMAIN
PROCESS --> FILESYSTEM
AUX --> FILESYSTEM
diff --git a/docs/capability-composition.md b/docs/capability-composition.md
new file mode 100644
index 0000000000..e39076993f
--- /dev/null
+++ b/docs/capability-composition.md
@@ -0,0 +1,107 @@
+# Capability composition and ownership
+
+REA keeps domain contracts and shared workflows separate from provider adapters
+and CLI/MCP translation. A capability can have several provider identities; a
+provider can contribute operations to several capabilities. Those associations
+are source facts, while host availability is an observation.
+
+## Source declarations and runtime bindings
+
+`src/composition/capabilityDeclarations.ts` contains static provider identities,
+named contract-group references and precise operation subsets. Complete groups
+refer to `TOOL_CONTRACT_GROUPS` in the existing contract owner; family definitions
+and provider catalogs read the same arrays, so a new group operation is included
+without updating a copied provider list. Metadata imports only the group ID type
+and pure metadata leaves; it must not load factories, schemas, engines or
+generated catalogs. Catalog generation resolves groups and subsets against the
+canonical contracts and rejects unknown groups/operations and conflicting
+identities. New provider provenance must come from the implementation
+that actually produces the Evidence, including format-specific capture adapters
+and derived workflows.
+
+`src/composition/capabilityServices.ts` binds typed service factories to those
+capability keys. Each runtime entry owns construction, cleanup and any available
+host/configuration facts or live probes. A service entry requires a close
+capability; entries adapting managers retain their existing `closeAll` operation.
+The host maps entries without erasing their individual service/port types. There
+is no generic JSON operation dispatcher.
+
+These two declarations have different loading needs. Static facts can be read
+without loading a schema or provider graph. The full runtime composition is
+materialized only when the MCP runtime needs it. One-shot CLI adapters import the
+selected factory rather than constructing a complete capability host. CLI and MCP
+reuse the same provider/service factories and application workflows, while
+keeping their respective argument translation and lifetime.
+
+## Resource lifetime
+
+`CapabilityScope` registers a module's actual close capability when its instance
+is composed. There is no independent server provider-close list to update.
+Concurrent shutdown calls join the same cleanup attempt. A successful peer is
+removed from pending cleanup; failed peers remain owned and are retried on the
+next close. Synchronous throws and rejections without a reason remain failures,
+and cannot prevent other peers from attempting cleanup. Shutdown seals further
+ownership registration.
+
+The host distinguishes these lifetimes:
+
+- Host-owned service instances are released by that host.
+- Caller-owned optional observations remain borrowed by SDK server instances;
+ their process-level owner closes them, including discarded discovery probes.
+- CLI invocations use their existing finalizers to join cleanup and preserve the
+ original observation, cancellation and cleanup errors.
+- Binary engines, bundle readers and investigation records retain their existing
+ owners. The `BinarySession` transition/drain barrier still coordinates target
+ changes, reset and disposal. A module refactor must not create independent
+ barriers that allow late work to repopulate reset records.
+
+An application workflow may borrow a shared resource scope, such as byte entropy
+using the host's artifact scope. Its wrapper does not acquire a second ownership
+claim over the borrowed scope.
+
+## Availability and registration
+
+Module bindings provide host support, selected configuration and precisely typed
+live availability probes through `CapabilityAvailabilityFacts`. Constructors do
+not run these probes or launch engines. Session discovery invokes the existing
+provider-owned probes only for advertised families; live failures remain fresh
+observations. Caller policy, selected environments and injected providers retain
+their existing precedence.
+
+MCP registration remains in the MCP adapter. It consumes composed services,
+record ports, admission and logging; it must not add fallback constructors that
+can escape the host owner. Named contracts bind handlers and schemas. The complete
+tool-family registrar map is checked by TypeScript, and catalog order never
+selects a handler or schema.
+
+## Extending a capability
+
+1. Add or reuse the domain contract and application workflow, with explicit
+ Evidence provenance, coverage, unknowns and caller choices.
+2. Put concrete construction in `src/composition/` and keep its default factory
+ lazy. Reference adapter-owned pure identity metadata from both the provider
+ and its static capability declaration.
+3. Add the runtime binding if a host-owned service is needed. Bind cleanup and
+ availability/probes there, preserving borrowed versus owned resources.
+4. Add the CLI/MCP argument adapters and their named registrations. A one-shot
+ command uses the shared selected factory and existing finalization helpers.
+5. Run source-boundary, discovery, SDK-schema and relevant public workflow checks.
+ Exercise partial cleanup, retry, cancellation and caller-owned peers at the
+ applicable production seam; use actual applications/providers for routing and
+ complete Evidence verification. Recording ports do not prove real providers.
+
+The constructor boundary guard covers the centralized Frida, Cutter, GDB and
+Rizin constructors without rejecting legitimate type-only injected interfaces.
+Other settled pure/provider boundaries continue checking type imports too. The
+shared parser records module-edge erasure once. Whole-declaration type imports
+are erased; inline type bindings retain their runtime module edge under
+`verbatimModuleSyntax`. Static dynamic imports remain runtime edges too.
+Tests belong with their semantic owner and must be
+found exactly once; provider roots need the corresponding adapter discovery lane.
+
+This is an internal composition contract. External plugin distribution, automatic
+provider installation and a universal provider interface are separate concerns.
+
+The [initial verification report](verification/capability-composition-2026-11-01.md)
+records the executed public workflows, cleanup boundaries, resource measurements
+and remaining provider qualification gaps for this composition increment.
diff --git a/docs/verification/capability-composition-2026-11-01.md b/docs/verification/capability-composition-2026-11-01.md
new file mode 100644
index 0000000000..f17a7c252c
--- /dev/null
+++ b/docs/verification/capability-composition-2026-11-01.md
@@ -0,0 +1,246 @@
+# Capability composition verification — 2026-11-01
+
+Scope: issue #8429, the service composition/ownership increment of #740. Baseline
+`6e9ee97a69cb16c7150d8b550e705cba055ee4f0`; implementation `bd9bf76d8`, followed
+by type-only binding cleanup `161c3c146`. This changes existing composition,
+metadata ownership and cleanup bookkeeping; no new provider/target authority.
+`BinarySession` keeps its existing transition/admission barrier.
+
+## Executed source and boundary checks
+
+Full typecheck, module boundaries, architecture guards, test discovery,
+production reachability, documentation consistency and repository formatting
+passed. Changed-file typed lint passed with one worker and `GOMEMLIMIT=512MiB`:
+39.622 s elapsed, 39.261 s process-family CPU, 2,945,368,064 B kernel peak.
+Initial whole-repository typed and ordinary lint attempts hit the configured
+3 GiB early stop; no OOM occurred. Whole-repository lint is not locally
+qualified; changed-file checks and the final ownership-boundary typed checks
+passed separately.
+
+Relevant executed groups: 216 parser/boundary/metadata/provider-owner tests;
+5 scope/factory tests; 27 MCP availability/composition tests; 28 deferred
+startup/protocol/publication tests; 50 CLI factory/cleanup/target-input tests;
+16 SDK schema tests; 4 compiled-verifier metadata link tests; and investigation
+lifecycle, unknown-history and transport cleanup regressions. These groups
+contain overlapping repeated checks and are not a unique total. The final
+Android transport group passed all 4 cases, including construction failure and
+failed cleanup retry. The moved Frida test body was preserved byte-for-byte;
+its discovery owner changed to the adapter lane.
+
+The old manual provider-close list was temporarily restored as a negative
+control: the new successful-peer cleanup regression failed, then the current
+source was restored. Synchronous close throws, undefined rejection reasons,
+concurrent close joining, borrowed optional observations and failed-owner retry
+are controlled tests, rather than claims of natural failures in real engines.
+An initial new transport assertion omitted the existing outer
+`OwnedMcpServers` AggregateError; its expected nesting was corrected. Other
+preserved harness failures were missing SDK catalog preparation and two wrong
+CLI test paths; corrected commands passed. One formatting issue was corrected.
+
+Ordinary Knip reports one unused exported `DarwinHelperCache` type at
+`src/process/DarwinHelperCache.ts:465`. An independent run on the clean pinned
+baseline reports the identical sole issue; that file is unchanged here.
+
+## Real applications and complete-result parity
+
+Every case used actual public CLI/MCP routes. Inputs were hashed before/after,
+with no source mutation. Both arms used pinned Node 24.22.1/npm 11.16.0 and
+fresh isolated processes/configuration; OS page caches were uncontrolled.
+
+| Domain | Genuine application/artifact | Independent fact oracle |
+| ---------- | ------------------------------------------------------------------- | ----------------------------------------------- |
+| Go | installed GitHub CLI `/usr/bin/gh`, Go 1.19.6 | ELF Go build-info bytes/module data |
+| Managed | SDK 8.0.427 `NuGet.CommandLine.XPlat.dll`, selected Program members | independent dnfile metadata/CIL facts |
+| JavaScript | installed agent-browser 0.27, selected entry source | original source declarations/imports |
+| Android | NewPipe 0.29.1 APK inventory/application graph | complete ZIP member census and 9 member digests |
+| WASM | actual CSS WASM artifact with WABT 1.0.42 | independent raw sections/types/exports |
+
+All five full public CLI Evidence results matched across arms. Explicit Go/JS
+per-arm bundle output paths were verified separately, and their complete saved
+parent Evidence matched. MCP performed 117 calls per arm, including six fresh
+producer calls, retained derivation, repeated close, checkpoint/reset, expected
+missing references, same-process restore and actual fresh-process restore.
+Complete producer bundles, full derived Evidence, exact requests/IDs and
+complete checkpoints matched across arms. The analyst Unknown revision 1→2
+history and every canonical mutation/history record remained authenticated;
+fresh re-exported checkpoints were byte-identical. Fresh consumers ran no
+producer calls. Ten independent domain fact assessments passed.
+
+| Completed workflow | Baseline elapsed / CPU (s) | Feature elapsed / CPU (s) | Baseline / feature kernel peak (B) |
+| ------------------------ | -------------------------- | ------------------------- | ---------------------------------- |
+| Five CLI tasks | 17.675 / 16.268 | 22.192 / 20.667 | 393,936,896 / 390,504,448 |
+| Five MCP lifecycle tasks | 68.270 / 65.500 | 61.201 / 58.041 | 731,680,768 / 776,331,264 |
+
+Times are single observations including setup and all calls, not a causal
+speedup/regression claim. Fresh-process tasks are reported separately from
+same-process/restored consumers; neither is a controlled cold OS-cache run.
+All tasks ran serially under a one-CPU, 4 GiB hard/3 GiB early-stop, no-swap,
+192-task guard. CPU and peak memory are kernel process-family counters at
+payload exit while the observer remained alive; all completed tasks verified
+owned cleanup and no OOM. Sampling/RSS is not substituted for these counters.
+Scripted checks do not incur model tokens; model usage is recorded separately.
+
+## Native engine and Codex CLI
+
+Real Ghidra 12.1.4/JDK 21 on `/usr/bin/jq` passed 37 public MCP calls per
+arm. Two new light address queries before/after healthy investigation reset
+returned identical Evidence. The same MCP client retained the same owned JVM
+PID, start time, process group and public run identity. Three retained native
+compositions used authenticated historical dossiers; these dossiers were not
+regenerated. Full derived results, original bundles and Unknown histories
+matched across arms and survived same/fresh-process restore.
+
+A mixed-routing task passed 58 calls per arm, with explicit `jq → APK → managed`
+metadata target switches and five new static producer families through the final
+registry. These metadata switches do not imply launching every target engine.
+Complete originals, derived results and checkpoint history matched across arms.
+
+| Completed task | Baseline elapsed / CPU (s) | Feature elapsed / CPU (s) | Baseline / feature kernel peak (B) |
+| --------------------------- | -------------------------- | ------------------------- | ---------------------------------- |
+| Ghidra/native lifecycle | 38.196 / 36.804 | 41.741 / 40.369 | 1,111,789,568 / 1,098,362,880 |
+| Mixed target/domain routing | 27.557 / 26.984 | 27.255 / 26.527 | 1,039,253,504 / 1,037,910,016 |
+
+The native task includes a fresh engine start; its second query is a healthy
+same-engine observation. Historical dossier reuse and fresh process consumers
+are separate phases. These are single observations with uncontrolled OS caches.
+
+Local `codex-cli 0.165.0` ran one actual `client-code` trial with the requested
+`gpt-6.1-terra` alias, reasoning `high`, and 19 selected tools. The resolved
+server-side model identity is not inferred. It completed 32 MCP calls across
+JavaScript, managed, Android and WASM: five new producer calls, retained
+compositions, four expected missing-reference controls, close/reset/save/import
+and Unknown revision 1→2. There were no unexpected MCP errors or timeout.
+
+The model's JS self-check remained explicitly **partial/unverified**: it used
+`JSON.stringify`, whose property ordering changed after canonical import.
+Independent full structural comparisons of all three actual results and the
+baseline proved identical fields, values, array order and Evidence ID. The
+first ordering difference was only the keys of
+`graph.nodes[0].observations[0].properties`. This distinction is retained in the
+assessment; the model's self-check is not relabeled a complete pass.
+
+An actual new, target-free MCP process replayed the model's checkpoint in
+9 calls. Complete Evidence and Unknown revision/mutation histories authenticated,
+and its 18,968,472 B export was byte-identical (17 Evidence records,
+2 Unknown revisions, 1 head), SHA-256
+`165188d74298f0173c5730973ecb5354a1b966ca54ec46ee177efa049ebbc755`.
+All four derived results and their complete producer records matched the
+independent baseline. The copied authentication profile was removed; runtime,
+production source and selected input hashes remained unchanged.
+
+Actual client usage for the single attempt: **336,490 input tokens**, of which
+**295,680 were cached**; **3,659 output tokens** and **250 reasoning output
+tokens** were reported. Cached input is a subset, not an additional token total.
+Baseline model usage was not measured; neither token savings nor a speedup is
+claimed. Scripted source-fact and fresh-process verifiers incur no model API
+tokens and are separate from this model usage.
+
+| Completed verification | Elapsed / process-family CPU (s) | Kernel family peak (B) |
+| ----------------------------------- | -------------------------------- | ---------------------- |
+| Actual Codex CLI trial | 74.644 / 26.818 | 1,061,597,184 |
+| Independent fresh checkpoint replay | 6.142 / 5.934 | 757,895,168 |
+
+Manual final-answer review verified the reported IDs, counts, digests, coverage
+and Unknown state against complete baseline values, all actual model calls and
+the independent replay. The outcome is supported with the explicit model JS
+self-check limitation resolved independently. Maximum observed in-flight MCP
+calls was one; no target command, web or file-change tool items occurred.
+Factual-assessment SHA-256:
+`33451102e5faf57f11c627481899f16c4fc47fa5cba5f12cd2ed781a3de54ccf`.
+Final immutable input/runtime audit SHA-256:
+`73abce493182a7b433b4b78737720640ceb41d6df435cb0fec915468a0a304c8`.
+
+## Catalog and build identity
+
+Both compact/full SDK catalogs are byte-identical to baseline: 234 tools,
+6 prompts, 180 CLI commands. The provider projection changes from 63 to 67 rows
+only for source-proven web-source/module tracing, Harmony and QLOG provenance
+corrections. All other product sections are unchanged. Its expected provider
+SHA-256 is `4234ed15d7e53f34599b59dfba1f678369c0ae95b2135c7ba3414d944ab156b6`.
+The 66 static declarations have 49 runtime keys and 17 explicit source-owned
+workflow/engine groups; 70 bindings union to 67 identities. Static imports reach
+44 pure modules without schema, engine or factory runtime imports.
+
+The baseline runtime is `0e94962f64f3f532cc96b2432916f328f60058e97cd0e8e25f5c3e6ed3cdec10`
+(2,670 files). Static pairs used clean `bd9bf76d8`, runtime
+`62018a023b185dd9c6839799ed1fe4f085810161c723e508fc817d2fccdb59e3`
+(2,713 files). Final type-only cleanup `161c3c146` rebuilt to
+`d7be1d17dc6b5c23f33468398656ac4d3961380368be1f70dc767f0ba1fac6bf`.
+The complete earlier manifest is cryptographically reconstructed by removing
+one emitted trailing comma in `dist/composition/capabilityServices.js`'s
+existing named import; every other runtime file is byte-identical. This is not
+a claim that the two runtime hashes are identical. Native/mixed/model cases use
+the final runtime.
+
+Raw inputs, outputs, commands, hashes, recorded failed checks and resource receipts
+remain outside Git under
+`/home/yann/yann/.rea-worktree-artifacts/20261101/capability-module-ownership/`.
+Static-pair audit receipt SHA-256:
+`801707569577c7cec848334cf72bfab26a7ff4a95c2a87872d71d78eef7765a3`.
+
+## Review corrections and final qualification
+
+Two actionable review findings were fixed at their producing owners:
+
+- `980991573`: import-edge facts now distinguish fully erased declarations from
+ inline type bindings, which still load their module under
+ `verbatimModuleSyntax`. Compiler-backed parser/boundary regressions passed;
+ the old parser fails the new expectations. A separate three-file probe through
+ the actually selected compiler emitted both inline runtime edges and erased
+ whole-declaration/type-query edges, without compiling the repository.
+- `de6a61636`: the existing contract owner exposes 47 named canonical groups.
+ Thirty-seven full provider bindings reference those shared arrays, while
+ 33 deliberate provider-subset/engine selections remain precise. The actual
+ ADB metadata now includes a new valid canonical ADB operation without an edit
+ to the provider declaration; reverting to the copied production lists fails
+ the regression. Unknown groups, unknown names and identity conflicts fail
+ catalog generation.
+
+Final full typecheck, module boundaries, test discovery and changed-file typed
+lint passed. Post-review source regressions passed 228 cases and SDK/MCP
+regressions passed 34; the two strengthened boundary/metadata files passed
+31 cases separately. These overlap earlier groups. Product catalog SHA-256
+remains exactly `e942c72047db651939dd47a31042e6818caaca7470092d63645801954a72b523`;
+both SDK catalogs remain byte-identical to the original baseline. Static metadata
+retains its pure loading boundary; canonical group IDs are imported only as types.
+
+The final implementation freeze is `de6a61636f3f84862cb4263c9ea219b7d2d4de36`,
+with 2,713 runtime files, SHA-256
+`be1d60c7dcca57eeb7ad09d682efd172c7f1679215e796a291b29eb52e29bda1`.
+Earlier model/engine measurements remain attributed to their executed revisions;
+they are not relabeled as runs on this later freeze.
+
+A new actual mixed-routing task on this final freeze passed all 58 calls:
+32.970 s elapsed, 32.180282 s process-family CPU, 1,042,538,496 B kernel peak.
+Independent five-domain source facts, complete producer records and all five
+full derived results/requests/IDs matched baseline and the prior qualified
+implementation. Same-process and true fresh-process restore preserved the full
+Unknown 1→2 mutation history and byte-identical 18,978,040 B checkpoint
+(20 Evidence records, 2 Unknown revisions, 1 head), SHA-256
+`0316bd6271c6d401a18c10d552b2005d39c98f3bbbc5945ae4823875af13fd5f`.
+Owned cleanup, runtime/production-source/input hashes and no OOM were verified.
+Append-only final-routing receipt SHA-256:
+`9c410c2c31bf38d7edc569deb95b93b7f9a2cf68a7b9a7f84d3ec924f0ad75cb`.
+This bounded requalification adds no model invocation or repeated native
+analysis query; earlier token measurements remain unchanged.
+
+CodeQL's six reported alert IDs (215, 490, 491, 493, 494, 495) all have open
+instances on exact-base main `6e9ee97`. All 24 SARIF paths were reviewed and
+independently checked: the new factory-default `process.env` sources retain the
+existing command/directory selection and explicit local authority. The shared
+filesystem/process sink files are byte-identical to base; Cutter's sink line
+shift only removes its old identity constant. No source suppression or global
+alert dismissal is included. The PR records the exact-base instance evidence
+and the owner's authorized exception for this unrelated existing-alert check.
+
+## Qualification limits
+
+NewPipe coverage is static inventory/graph, not real JADX decompilation: JADX,
+apktool and aapt2 were unavailable. No genuine local GGUF/safetensors target was
+available. Real Hopper, IDA, JEB, Cutter GUI, GDB/Rizin debugger and Frida target
+attachment are not qualified by these application cases; relevant factory,
+protocol and cleanup tests use controlled seams. Historical native dossiers
+are explicitly labeled reused evidence, not newly reproduced function analysis.
+Retained records remain bounded by existing investigation policies; this does
+not promise unlimited retention or a new durability backend. Further module
+qualification and `BinarySession` simplification remain under parent #740.
diff --git a/scripts/lib/catalog-builders.d.mts b/scripts/lib/catalog-builders.d.mts
new file mode 100644
index 0000000000..d400f62084
--- /dev/null
+++ b/scripts/lib/catalog-builders.d.mts
@@ -0,0 +1,57 @@
+interface ProviderBinding {
+ readonly identity: {
+ readonly id: string;
+ readonly name: string;
+ readonly version: string | null;
+ };
+ readonly operations: readonly string[] | { readonly group: string };
+}
+
+interface ProviderCatalogSources {
+ readonly toolContracts: {
+ readonly TOOL_CONTRACTS: readonly { readonly name: string }[];
+ };
+ readonly toolFamilies: {
+ readonly TOOL_CONTRACT_GROUPS: Readonly<
+ Record
+ >;
+ };
+ readonly capabilityDeclarations: {
+ readonly CAPABILITY_DECLARATIONS: Readonly<
+ Record
+ >;
+ };
+}
+
+export function providerCatalog(sources: ProviderCatalogSources): {
+ readonly id: string;
+ readonly name: string;
+ readonly version: string | null;
+ readonly capabilities: readonly string[];
+}[];
+
+export function toolFamilyCatalog(sources: {
+ readonly toolFamilies: {
+ readonly TOOL_FAMILY_DEFINITIONS: readonly {
+ readonly id: string;
+ readonly surface: string;
+ readonly contracts: readonly { readonly name: string }[];
+ }[];
+ };
+ readonly toolContracts: {
+ readonly TOOL_CONTRACTS: readonly unknown[];
+ };
+ readonly catalogIdentity: {
+ readonly FULL_CATALOG_IDENTITY: {
+ readonly counts: { readonly mcp_tools: number };
+ };
+ };
+}): {
+ readonly total: number;
+ readonly families: readonly {
+ readonly id: string;
+ readonly surface: string;
+ readonly count: number;
+ readonly tools: readonly string[];
+ }[];
+};
diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs
index 1e59826ac0..cb42ebba92 100644
--- a/scripts/lib/catalog-builders.mjs
+++ b/scripts/lib/catalog-builders.mjs
@@ -23,414 +23,55 @@ export const toolFamilyCatalog = (sources) => {
return { total: familyTotal, families };
};
-/** Build provider identities with their sorted capability names. */
+/** Derive provider capabilities from the same static declarations used by composition. */
export const providerCatalog = (sources) => {
- const contractsByName = new Map(
- sources.toolContracts.TOOL_CONTRACTS.map((contract) => [
- contract.name,
- contract,
- ]),
+ const contractNames = new Set(
+ sources.toolContracts.TOOL_CONTRACTS.map(({ name }) => name),
);
- const declaredContracts = (providerId, operations) =>
- operations.map((operation) => {
- const contract = contractsByName.get(operation);
- if (contract === undefined)
+ const providers = new Map();
+ for (const declaration of Object.values(
+ sources.capabilityDeclarations.CAPABILITY_DECLARATIONS,
+ )) {
+ for (const { identity, operations } of declaration.providers) {
+ let provider = providers.get(identity.id);
+ if (provider === undefined) {
+ provider = { ...identity, capabilities: new Set() };
+ providers.set(identity.id, provider);
+ } else if (
+ provider.name !== identity.name ||
+ provider.version !== identity.version
+ ) {
throw new TypeError(
- `Provider ${providerId} declares an unknown analyst operation: ${operation}`,
+ `Provider ${identity.id} has conflicting source identities`,
);
- return contract;
- });
- const {
- applicationContracts,
- reconciliationContracts,
- observationContracts,
- activeContracts,
- fuseContracts,
- } = electronContractSlices(sources);
- return [
- {
- identity: sources.cutterProvider.CUTTER_PROVIDER_IDENTITY,
- contracts: sources.cutterContracts.CUTTER_TOOL_CONTRACTS,
- },
- {
- identity: sources.objdumpProvider.OBJDUMP_PROVIDER_IDENTITY,
- contracts:
- sources.reverseEngineeringContracts.REVERSE_ENGINEERING_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_with_objdump",
- ),
- },
- {
- identity: sources.rizinProvider.RIZIN_PROVIDER_IDENTITY,
- contracts:
- sources.reverseEngineeringContracts.REVERSE_ENGINEERING_TOOL_CONTRACTS.filter(
- ({ name }) => name === "execute_rizin_command",
- ),
- },
- {
- identity: sources.gdbProvider.GDB_PROVIDER_IDENTITY,
- contracts: sources.gdbContracts.GDB_TOOL_CONTRACTS,
- },
- {
- identity: sources.rizinDebugProvider.RIZIN_DEBUGGER_PROVIDER_IDENTITY,
- contracts: sources.rizinDebugContracts.RIZIN_DEBUG_TOOL_CONTRACTS,
- },
- {
- identity: sources.fridaProvider.FRIDA_PROVIDER_IDENTITY,
- contracts: sources.fridaContracts.FRIDA_TOOL_CONTRACTS,
- },
- {
- identity: sources.hopperProvider.HOPPER_PROVIDER_IDENTITY,
- contracts: declaredContracts(
- "hopper",
- sources.hopperProvider.HOPPER_OPERATIONS,
+ }
+ let selectedOperations = operations;
+ if (!Array.isArray(operations)) {
+ const contracts =
+ sources.toolFamilies.TOOL_CONTRACT_GROUPS[operations.group];
+ if (contracts === undefined)
+ throw new TypeError(
+ `Provider ${identity.id} declares an unknown contract group: ${operations.group}`,
+ );
+ selectedOperations = contracts.map(({ name }) => name);
+ }
+ for (const operation of selectedOperations) {
+ if (!contractNames.has(operation))
+ throw new TypeError(
+ `Provider ${identity.id} declares an unknown analyst operation: ${operation}`,
+ );
+ provider.capabilities.add(operation);
+ }
+ }
+ }
+ return [...providers.values()]
+ .map(({ id, name, version, capabilities }) => ({
+ id,
+ name,
+ version,
+ capabilities: [...capabilities].sort((left, right) =>
+ left.localeCompare(right),
),
- },
- {
- identity: sources.ghidraProvider.GHIDRA_PROVIDER_IDENTITY,
- contracts: declaredContracts(
- "ghidra",
- sources.ghidraProvider.GHIDRA_OPERATIONS,
- ),
- },
- {
- identity: sources.idaProvider.IDA_PROVIDER_IDENTITY,
- contracts: declaredContracts("ida", sources.idaProvider.IDA_OPERATIONS),
- },
- {
- identity: sources.kunaProvider.KUNA_PROVIDER_IDENTITY,
- contracts: declaredContracts(
- "kuna",
- sources.kunaProvider.KUNA_OPERATIONS,
- ),
- },
- {
- identity: sources.nativeProvider.NATIVE_MACOS_PROVIDER_IDENTITY,
- contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
- },
- {
- identity: sources.pwntoolsProvider.PWNTOOLS_PROVIDER_IDENTITY,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) =>
- name === "inspect_binary_layout" ||
- name === "inspect_recorded_crash",
- ),
- },
- {
- identity: sources.artifactProviders.PE_RESOURCES_PROVIDER,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_pe_resources",
- ),
- },
- {
- identity: sources.artifactProviders.PE_CERTIFICATES_PROVIDER,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_pe_certificates",
- ),
- },
- {
- identity: sources.artifactProviders.PE_IMPORTS_PROVIDER,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_pe_imports",
- ),
- },
- {
- identity: sources.artifactProviders.DEVICETREE_PROVIDER,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_devicetree",
- ),
- },
- {
- identity: sources.artifactProviders.PE_LOAD_CONFIG_PROVIDER,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_pe_load_config",
- ),
- },
- {
- identity: sources.artifactProviders.ELF_NOTES_PROVIDER,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_elf_notes",
- ),
- },
- {
- identity: sources.artifactProviders.APPLE_IPS_PROVIDER,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_apple_ips",
- ),
- },
- {
- identity: sources.artifactProviders.KERNEL_MODULE_PROVIDER,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_kernel_module",
- ),
- },
- {
- identity: sources.artifactProviders.BYTE_ENTROPY_PROVIDER,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_byte_entropy",
- ),
- },
- {
- identity: sources.artifactProviders.NATIVE_EXPORTS_PROVIDER,
- contracts:
- sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_native_exports",
- ),
- },
- {
- identity: sources.captureProvider.TSHARK_PROVIDER_IDENTITY,
- contracts: sources.captureContracts.CAPTURE_TOOL_CONTRACTS,
- },
- {
- identity: sources.signalProvider.SIGROK_PROVIDER_IDENTITY,
- contracts: sources.signalContracts.SIGNAL_TOOL_CONTRACTS,
- },
- {
- identity: sources.wasmProvider.WABT_PROVIDER_IDENTITY,
- contracts: sources.wasmContracts.WASM_TOOL_CONTRACTS.filter(
- ({ kind }) => kind === "artifact-provider",
- ),
- },
- {
- identity: sources.apkSignatureProvider.APK_SIGNATURE_PROVIDER_IDENTITY,
- contracts: sources.apkSignatureContracts.APK_SIGNATURE_TOOL_CONTRACTS,
- },
- {
- identity: sources.evmProvider.EVMOLE_PROVIDER_IDENTITY,
- contracts: sources.evmContracts.EVM_TOOL_CONTRACTS,
- },
- {
- identity: sources.goProvider.GO_BINARY_PROVIDER_IDENTITY,
- contracts: sources.goContracts.GO_TOOL_CONTRACTS,
- },
- {
- identity: sources.gpuProvider.GPU_CODE_OBJECTS_PROVIDER_IDENTITY,
- contracts: sources.gpuContracts.GPU_TOOL_CONTRACTS,
- },
- {
- identity: sources.il2cppProvider.IL2CPP_METADATA_PROVIDER_IDENTITY,
- contracts: sources.il2cppContracts.IL2CPP_TOOL_CONTRACTS,
- },
- {
- identity: sources.modelProvider.MODEL_ARTIFACT_PROVIDER_IDENTITY,
- contracts: sources.modelContracts.MODEL_TOOL_CONTRACTS,
- },
- {
- identity: sources.rustProvider.RUST_BINARY_PROVIDER_IDENTITY,
- contracts: sources.rustContracts.RUST_TOOL_CONTRACTS,
- },
- {
- identity: sources.pythonPycProvider.PYTHON_PYC_PROVIDER_IDENTITY,
- contracts: sources.pythonPycContracts.PYTHON_PYC_TOOL_CONTRACTS,
- },
- {
- identity: sources.erlangProvider.ERLANG_BEAM_PROVIDER_IDENTITY,
- contracts: sources.erlangContracts.ERLANG_BEAM_TOOL_CONTRACTS,
- },
- {
- identity: sources.electronFuseProvider.ELECTRON_FUSE_PROVIDER_IDENTITY,
- contracts: fuseContracts,
- },
- {
- identity: sources.sqliteDomain.SQLITE_PROVIDER_IDENTITY,
- contracts: sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
- },
- {
- identity: sources.artifactProviders.ARTIFACT_GRAPH_PROVIDER,
- contracts: [
- ...sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
- ...sources.desktopContracts.DESKTOP_APPLICATION_TOOL_CONTRACTS,
- ],
- },
- {
- identity: sources.artifactProviders.MANAGED_STATIC_PROVIDER,
- contracts: sources.managedContracts.MANAGED_TOOL_CONTRACTS,
- },
- {
- identity: sources.artifactProviders.MANAGED_WORKFLOW_PROVIDER,
- contracts:
- sources.managedWorkflowContracts.MANAGED_WORKFLOW_TOOL_CONTRACTS,
- },
- {
- identity: sources.firmwareProvider.BINWALK_PROVIDER_IDENTITY,
- contracts: sources.firmwareContracts.FIRMWARE_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_firmware_regions",
- ),
- },
- {
- identity: sources.firmwareProvider.UNBLOB_PROVIDER_IDENTITY,
- contracts: sources.firmwareContracts.FIRMWARE_TOOL_CONTRACTS.filter(
- ({ name }) => name === "extract_firmware",
- ),
- },
- {
- identity: sources.androidProvider.JADX_PROVIDER_IDENTITY,
- contracts: sources.androidContracts.ANDROID_TOOL_CONTRACTS,
- },
- {
- identity: sources.pcapProvider.PCAP_PROVIDER_IDENTITY,
- contracts: sources.pcapContracts.PCAP_TOOL_CONTRACTS,
- },
- {
- identity: sources.flutterProvider.FLUTTER_PROVIDER_IDENTITY,
- contracts: sources.flutterContracts.FLUTTER_TOOL_CONTRACTS,
- },
- {
- identity: sources.apktoolProvider.APKTOOL_PROVIDER_IDENTITY,
- contracts: sources.apktoolContracts.APKTOOL_TOOL_CONTRACTS,
- },
- {
- identity: sources.jebProvider.JEB_PROVIDER_IDENTITY,
- contracts: sources.jebContracts.JEB_TOOL_CONTRACTS,
- },
- {
- identity: sources.adbProvider.ADB_PROVIDER_IDENTITY,
- contracts: sources.adbContracts.ADB_TOOL_CONTRACTS,
- },
- {
- identity: sources.webNetworkCaptureProvider.HAR_CAPTURE_PROVIDER_IDENTITY,
- contracts:
- sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
- },
- {
- identity: sources.webNetworkCaptureProvider.SAZ_CAPTURE_PROVIDER_IDENTITY,
- contracts:
- sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
- },
- {
- identity:
- sources.webNetworkCaptureProvider.MITMPROXY_CAPTURE_PROVIDER_IDENTITY,
- contracts:
- sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
- },
- {
- identity: sources.browserIdentities.CDP_BROWSER_PROVIDER_IDENTITY,
- contracts: [
- ...sources.browserContracts.BROWSER_TOOL_CONTRACTS,
- ...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS,
- ],
- },
- {
- identity:
- sources.browserIdentities.PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
- contracts:
- sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS,
- },
- {
- identity: sources.browserIdentities.CDP_ELECTRON_PROVIDER_IDENTITY,
- contracts: observationContracts,
- },
- {
- identity:
- sources.browserIdentities.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
- contracts: activeContracts,
- },
- {
- identity: sources.v8InspectorProvider.V8_INSPECTOR_PROVIDER_IDENTITY,
- contracts:
- sources.javascriptRuntimeObservationContracts
- .JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS,
- },
- {
- identity: sources.artifactProviders.JAVASCRIPT_APPLICATION_PROVIDER,
- contracts: applicationContracts,
- },
- {
- identity:
- sources.artifactProviders.JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER,
- contracts: reconciliationContracts,
- },
- {
- identity:
- sources.artifactProviders.JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER,
- contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
- ({ name }) =>
- name !== "project_android_application_graph" &&
- name !== "project_apple_application_graph",
- ),
- },
- {
- identity: sources.artifactProviders.ANALYSIS_VIEW_PROVIDER,
- contracts: sources.analysisViewContracts.ANALYSIS_VIEW_TOOL_CONTRACTS,
- },
- {
- identity: sources.artifactProviders.ANDROID_APPLICATION_PROVIDER,
- contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
- ({ name }) => name === "project_android_application_graph",
- ),
- },
- {
- identity: sources.artifactProviders.APPLE_APPLICATION_PROVIDER,
- contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
- ({ name }) => name === "project_apple_application_graph",
- ),
- },
- {
- identity: sources.artifactProviders.WEB_SCRIPT_EXPORT_PROVIDER,
- contracts: sources.webScriptContracts.WEB_SCRIPT_TOOL_CONTRACTS,
- },
- {
- identity: sources.javascriptRecoveryProvider.WAKARU_PROVIDER_IDENTITY,
- contracts:
- sources.javascriptRecoveryContracts.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
- },
- ]
- .map(({ identity, contracts }) => ({
- id: identity.id,
- name: identity.name,
- version: identity.version,
- capabilities: names(contracts),
}))
.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" &&
- name !== "inspect_electron_fuses",
- );
- const activeContracts =
- sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
- ({ name }) => name === "capture_electron_scenario",
- );
- const fuseContracts =
- sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
- ({ name }) => name === "inspect_electron_fuses",
- );
- if (
- applicationContracts.length !== 1 ||
- reconciliationContracts.length !== 1 ||
- observationContracts.length !== 2 ||
- activeContracts.length !== 1 ||
- fuseContracts.length !== 1
- )
- throw new Error("Electron provider capability ownership drifted");
- return {
- applicationContracts,
- reconciliationContracts,
- observationContracts,
- activeContracts,
- fuseContracts,
- };
-};
diff --git a/scripts/lib/catalog-core.mjs b/scripts/lib/catalog-core.mjs
index 102a9e3c1c..81ca68692e 100644
--- a/scripts/lib/catalog-core.mjs
+++ b/scripts/lib/catalog-core.mjs
@@ -39,131 +39,8 @@ const SOURCE_PATHS = {
cli: "dist/cli.js",
toolContracts: "dist/contracts/toolContracts.js",
toolFamilies: "dist/contracts/toolFamilies.js",
- binaryDiagnosticsContracts:
- "dist/contracts/native/binaryDiagnosticsToolContracts.js",
- cutterContracts: "dist/contracts/cutterToolContracts.js",
- cutterProvider: "dist/cutter/CutterBridgeClient.js",
- reverseEngineeringContracts:
- "dist/contracts/reverseEngineeringToolContracts.js",
- gdbContracts: "dist/contracts/gdbToolContracts.js",
- gdbProvider: "dist/gdb/GdbSessionManager.js",
- rizinDebugContracts: "dist/contracts/rizinDebugToolContracts.js",
- objdumpProvider: "dist/objdump/ObjdumpCommand.js",
- rizinProvider: "dist/rizin/RizinCommand.js",
- rizinDebugProvider: "dist/rizin/RizinDebugSessionManager.js",
- fridaContracts: "dist/contracts/fridaToolContracts.js",
- fridaProvider: "dist/frida/FridaInstrumentationManager.js",
- pwntoolsProvider: "dist/native/pwntools/PwntoolsRelease.js",
- nativeContracts: "dist/contracts/native/nativeToolContracts.js",
- captureContracts: "dist/contracts/captures/captureToolContracts.js",
- captureProvider: "dist/captures/TsharkCaptureProvider.js",
- signalContracts: "dist/contracts/signals/signalToolContracts.js",
- signalProvider: "dist/signals/SigrokRecordingProvider.js",
- wasmContracts: "dist/contracts/wasm/wasmToolContracts.js",
- wasmProvider: "dist/wasm/WabtRelease.js",
- apkSignatureContracts:
- "dist/contracts/apkSignature/apkSignatureToolContracts.js",
- apkSignatureProvider: "dist/apkSignature/ApkSignatureRelease.js",
- evmContracts: "dist/contracts/evm/evmToolContracts.js",
- sqliteContracts: "dist/contracts/sqlite/sqliteToolContracts.js",
- sqliteDomain: "dist/domain/sqlite/sqliteDatabase.js",
- evmProvider: "dist/evm/EvmoleRelease.js",
- goContracts: "dist/contracts/go/goToolContracts.js",
- goProvider: "dist/go/GoBinaryProvider.js",
- gpuContracts: "dist/contracts/gpu/gpuToolContracts.js",
- gpuProvider: "dist/gpu/GpuCodeObjectsProvider.js",
- modelContracts: "dist/contracts/model/modelToolContracts.js",
- modelProvider: "dist/model/ModelArtifactProvider.js",
- il2cppContracts: "dist/contracts/il2cpp/il2cppToolContracts.js",
- il2cppProvider: "dist/il2cpp/Il2cppMetadataProvider.js",
- rustContracts: "dist/contracts/rust/rustToolContracts.js",
- rustProvider: "dist/rust/RustBinaryProvider.js",
- pythonPycContracts: "dist/contracts/python/pythonPycToolContracts.js",
- pythonPycProvider: "dist/python/PythonPycProvider.js",
- erlangContracts: "dist/contracts/erlang/beamToolContracts.js",
- erlangProvider: "dist/erlang/BeamProvider.js",
- artifactContracts: "dist/contracts/artifactToolContracts.js",
- desktopContracts: "dist/contracts/desktopApplicationToolContracts.js",
- managedContracts: "dist/contracts/managed/managedToolContracts.js",
- firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",
- firmwareProvider: "dist/firmware/FirmwareRelease.js",
- androidContracts: "dist/contracts/android/androidToolContracts.js",
- flutterContracts: "dist/contracts/flutter/flutterToolContracts.js",
- flutterProvider: "dist/flutter/FlutterBuildProvider.js",
- androidProvider: "dist/android/JadxRelease.js",
- apktoolContracts: "dist/contracts/apktool/apktoolToolContracts.js",
- pcapContracts: "dist/contracts/pcap/pcapToolContracts.js",
- pcapProvider: "dist/pcap/PcapProvider.js",
- apktoolProvider: "dist/apktool/ApktoolProvider.js",
- jebContracts: "dist/contracts/jeb/jebToolContracts.js",
- jebProvider: "dist/jeb/JebProvider.js",
- adbContracts: "dist/contracts/adb/adbToolContracts.js",
- adbProvider: "dist/adb/AdbProvider.js",
- managedWorkflowContracts:
- "dist/contracts/managed/managedWorkflowToolContracts.js",
- browserContracts: "dist/contracts/browserToolContracts.js",
- webRuntimeContracts: "dist/contracts/webRuntimeToolContracts.js",
- webNetworkCaptureContracts:
- "dist/contracts/webNetworkCaptureToolContracts.js",
- webNetworkCaptureProvider: "dist/browser/history/CaptureRelease.js",
- browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js",
- electronContracts: "dist/contracts/javascript/electronToolContracts.js",
- electronFuseProvider: "dist/electron/ElectronFuseProvider.js",
- javascriptRuntimeObservationContracts:
- "dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js",
- applicationContracts: "dist/contracts/applicationToolContracts.js",
- analysisViewContracts: "dist/contracts/analysisViewToolContracts.js",
- webScriptContracts: "dist/contracts/webScriptToolContracts.js",
- javascriptRecoveryContracts:
- "dist/contracts/javascript/javascriptRecoveryToolContracts.js",
- javascriptRecoveryProvider: "dist/javascript/recovery/WakaruRelease.js",
+ capabilityDeclarations: "dist/composition/capabilityDeclarations.js",
supportedClients: "dist/application/SupportedClients.js",
- hopperProvider: "dist/hopper/HopperProviderCapabilities.js",
- ghidraProvider: "dist/ghidra/GhidraProviderCapabilities.js",
- idaProvider: "dist/ida/IdaProviderCapabilities.js",
- kunaProvider: "dist/kuna/KunaProviderCapabilities.js",
- nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
- artifactProviders: "dist/domain/investigationProviders.js",
- browserIdentities: "dist/browser/providerIdentities.js",
- v8InspectorProvider: "dist/inspector/providerIdentity.js",
- evidence: "dist/domain/evidence.js",
- evidenceBundle: "dist/domain/evidenceBundle.js",
- processCapture: "dist/domain/process/processCapture.js",
- analysisSnapshot: "dist/domain/analysisSnapshot.js",
- artifactGraph: "dist/domain/artifactGraph.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/javascript/electronObservation.js",
- electronActiveObservation:
- "dist/domain/javascript/electronActiveObservation.js",
- javascriptRuntimeObservation:
- "dist/domain/javascript/javascriptRuntimeObservation.js",
- webBundleAnalysis: "dist/domain/webBundleAnalysis.js",
- webCaptureDiff: "dist/domain/webCaptureDiff.js",
- managedArtifact: "dist/domain/managed/managedArtifact.js",
- managedComparison: "dist/domain/managed/managedMemberComparison.js",
- managedNativeVerification: "dist/domain/managed/managedNativeVerification.js",
- webMcpDiscovery: "dist/domain/webMcpDiscovery.js",
- webScreenshot: "dist/domain/webScreenshot.js",
- javascriptApplicationGraph:
- "dist/domain/javascript/javascriptApplicationGraph.js",
- javascriptApplicationAnalysis:
- "dist/domain/javascript/javascriptApplicationAnalysis.js",
- javascriptSemanticGraph: "dist/domain/javascript/javascriptSemanticGraph.js",
- javascriptSemanticQuery:
- "dist/domain/javascript/javascriptSemanticQuerySchemas.js",
- javascriptSemanticTrace:
- "dist/domain/javascript/javascriptSemanticTraceSchemas.js",
- javascriptFeatureTrace:
- "dist/domain/javascript/javascriptFeatureTraceSchemas.js",
- javascriptVersionComparison:
- "dist/domain/javascript/javascriptApplicationVersionComparisonSchemas.js",
- reconstructionVerification:
- "dist/domain/reconstructionVerificationSchemas.js",
- residualUnknown: "dist/domain/residualUnknown.js",
};
/** Load every compiled source module referenced by the catalog. */
diff --git a/scripts/lib/import-specifiers.d.mts b/scripts/lib/import-specifiers.d.mts
index ae29fe371b..beeef8cbba 100644
--- a/scripts/lib/import-specifiers.d.mts
+++ b/scripts/lib/import-specifiers.d.mts
@@ -2,6 +2,8 @@
export interface ImportSpecifierFact {
readonly specifier: string;
readonly line: number;
+ /** True only when TypeScript erases this module edge under verbatimModuleSyntax. */
+ readonly typeOnly: boolean;
}
/** Static string specifier on one import or re-export node, when it has one. */
diff --git a/scripts/lib/import-specifiers.mjs b/scripts/lib/import-specifiers.mjs
index 776b0440da..0f7877dd39 100644
--- a/scripts/lib/import-specifiers.mjs
+++ b/scripts/lib/import-specifiers.mjs
@@ -26,6 +26,22 @@ export const importSpecifier = (node) => {
}
};
+/** Whether TypeScript erases this module edge under verbatimModuleSyntax. */
+const isTypeOnly = (node) => {
+ switch (node.type) {
+ case "TSImportType":
+ return true;
+ case "ImportDeclaration":
+ case "TSImportEqualsDeclaration":
+ return node.importKind === "type";
+ case "ExportNamedDeclaration":
+ case "ExportAllDeclaration":
+ return node.exportKind === "type";
+ default:
+ return false;
+ }
+};
+
/**
* Static import and re-export specifiers in source order.
* Comments and string literals are not imports. Non-string dynamic imports
@@ -37,7 +53,11 @@ export const readImportSpecifiers = (source) => {
traverseFast(tree, (node) => {
const specifier = importSpecifier(node);
if (specifier === undefined) return;
- specifiers.push({ specifier, line: node.loc?.start.line ?? 1 });
+ specifiers.push({
+ specifier,
+ line: node.loc?.start.line ?? 1,
+ typeOnly: isTypeOnly(node),
+ });
});
return specifiers;
};
diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs
index a5dbc26615..7277d9e44c 100644
--- a/scripts/lib/module-boundaries.mjs
+++ b/scripts/lib/module-boundaries.mjs
@@ -24,6 +24,8 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
"inspector",
"javascript",
]);
+const CENTRALIZED_CONSTRUCTOR_OWNER =
+ /^src\/(?:frida\/FridaInstrumentationManager|cutter\/CutterBridge(?:Client|Service)|gdb\/GdbSessionManager|rizin\/RizinDebugSessionManager)\.(?:js|ts)$/u;
const repositoryPath = (root, path) =>
relative(root, path).replaceAll("\\", "/");
@@ -35,7 +37,7 @@ const sourceTarget = (file, specifier) => {
: undefined;
};
-const failedBoundary = (file, target) => {
+const failedBoundary = (file, target, typeOnly) => {
if (!file.startsWith("src/") || !target.startsWith("src/")) return undefined;
const layer = file.split("/")[1];
const targetLayer = target.split("/")[1];
@@ -75,6 +77,12 @@ const failedBoundary = (file, target) => {
}
if (layer === "application" && targetLayer === "composition")
return "application-composition";
+ if (
+ (layer === "application" || layer === "server") &&
+ !typeOnly &&
+ CENTRALIZED_CONSTRUCTOR_OWNER.test(target)
+ )
+ return "provider-construction";
if (
(layer === "application" || layer === "server") &&
(MIGRATED_PROVIDER_ROOTS.has(targetLayer) ||
@@ -92,11 +100,11 @@ export const inspectModuleBoundaries = (file, source, root) => {
const absoluteFile = resolve(root, file);
const importer = repositoryPath(root, absoluteFile);
const violations = [];
- for (const { specifier, line } of readImportSpecifiers(source)) {
+ for (const { specifier, line, typeOnly } of readImportSpecifiers(source)) {
const resolved = sourceTarget(absoluteFile, specifier);
if (resolved === undefined) continue;
const target = repositoryPath(root, resolved);
- const boundary = failedBoundary(importer, target);
+ const boundary = failedBoundary(importer, target, typeOnly);
if (boundary !== undefined)
violations.push({ file: importer, line, specifier, target, boundary });
}
diff --git a/scripts/lib/real-hopper-inventory.mjs b/scripts/lib/real-hopper-inventory.mjs
index 30cb5f8a4d..9b9ea51fd2 100644
--- a/scripts/lib/real-hopper-inventory.mjs
+++ b/scripts/lib/real-hopper-inventory.mjs
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
-import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js";
+import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderMetadata.js";
import {
requireMcpEvidenceResult,
requireEvidenceProvider,
diff --git a/scripts/lib/real-hopper-selection.mjs b/scripts/lib/real-hopper-selection.mjs
index 5206c0ad61..3e4c858773 100644
--- a/scripts/lib/real-hopper-selection.mjs
+++ b/scripts/lib/real-hopper-selection.mjs
@@ -1,4 +1,4 @@
-import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js";
+import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderMetadata.js";
/** Verify the selected Hopper candidate and concrete provider binding. */
export const requireHopperSelection = (status, expected) => {
diff --git a/scripts/lib/test-projects.mjs b/scripts/lib/test-projects.mjs
index 627254fd42..6e0be70299 100644
--- a/scripts/lib/test-projects.mjs
+++ b/scripts/lib/test-projects.mjs
@@ -19,7 +19,7 @@ export const SOURCE_TEST_PROJECTS = [
group: "source",
include: [
"src/*.test.ts",
- "src/{android,apkSignature,apktool,jeb,adb,artifacts,browser,config,cutter,dotnet,electron,erlang,evm,firmware,flutter,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,main,native,process,providers,python,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts",
+ "src/{android,apkSignature,apktool,jeb,adb,artifacts,browser,config,cutter,dotnet,electron,erlang,evm,firmware,flutter,frida,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,main,native,process,providers,python,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts",
],
},
{
diff --git a/scripts/verify-packaged-ghidra-windows.mjs b/scripts/verify-packaged-ghidra-windows.mjs
index e48f1b546e..c37da5b673 100644
--- a/scripts/verify-packaged-ghidra-windows.mjs
+++ b/scripts/verify-packaged-ghidra-windows.mjs
@@ -510,7 +510,7 @@ try {
);
const { GHIDRA_OPERATIONS } = await import(
pathToFileURL(
- join(packageRoot, "dist/ghidra/GhidraProviderCapabilities.js"),
+ join(packageRoot, "dist/ghidra/GhidraProviderMetadata.js"),
)
);
const expected = GHIDRA_OPERATIONS.filter(
diff --git a/scripts/verify-real-ghidra-windows.mjs b/scripts/verify-real-ghidra-windows.mjs
index ca64f38040..bc8062206d 100644
--- a/scripts/verify-real-ghidra-windows.mjs
+++ b/scripts/verify-real-ghidra-windows.mjs
@@ -8,20 +8,20 @@ import { fileURLToPath } from "node:url";
import { parseConfig } from "../dist/config/parseConfig.js";
import { resolveGhidraAnalysisProfile } from "../dist/ghidra/GhidraAnalysisProfile.js";
import { GhidraClient } from "../dist/ghidra/GhidraClient.js";
+import { GHIDRA_FUNCTION_OPERATIONS } from "../dist/ghidra/GhidraFunctionOperations.js";
import {
- GHIDRA_FUNCTION_OPERATIONS,
parseGhidraFunctionInput,
parseGhidraFunctionResult,
} from "../dist/ghidra/GhidraFunctionValues.js";
+import { GHIDRA_INVENTORY_OPERATIONS } from "../dist/ghidra/GhidraInventoryOperations.js";
import {
- GHIDRA_INVENTORY_OPERATIONS,
parseGhidraInventoryInput,
parseGhidraInventoryResult,
} from "../dist/ghidra/GhidraInventoryValues.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraReleasePolicy.js";
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
-import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js";
+import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderMetadata.js";
import {
WINDOWS_P0_NATIVE_PE_ARCHITECTURES,
windowsP0Capabilities,
diff --git a/scripts/verify-real-ghidra.mjs b/scripts/verify-real-ghidra.mjs
index 612e4f9f14..2d4aa94e9b 100644
--- a/scripts/verify-real-ghidra.mjs
+++ b/scripts/verify-real-ghidra.mjs
@@ -13,7 +13,7 @@ import { GhidraClient } from "../dist/ghidra/GhidraClient.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraReleasePolicy.js";
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
-import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js";
+import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderMetadata.js";
import {
assertCleanup,
diff --git a/scripts/verify-real-hopper-deadlines.mjs b/scripts/verify-real-hopper-deadlines.mjs
index af2213ec6f..77f3b6b329 100644
--- a/scripts/verify-real-hopper-deadlines.mjs
+++ b/scripts/verify-real-hopper-deadlines.mjs
@@ -11,7 +11,7 @@ import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js"
import { resolveHopperAnalysisProfile } from "../dist/hopper/HopperAnalysisProfile.js";
import { HopperApplicationLauncher } from "../dist/hopper/BridgeLauncher.js";
import { HopperClient } from "../dist/hopper/HopperClient.js";
-import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js";
+import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderMetadata.js";
import { loadRealHopperFixtureTargets } from "./lib/real-hopper-fixture.mjs";
// These options belong to the native client, below the CLI/MCP tool surface.
diff --git a/scripts/verify-real-hopper.mjs b/scripts/verify-real-hopper.mjs
index 5f31565b4a..6e6cce01e5 100644
--- a/scripts/verify-real-hopper.mjs
+++ b/scripts/verify-real-hopper.mjs
@@ -11,7 +11,7 @@ import {
requireFunctionDossier,
requirePseudocode,
} from "./lib/real-hopper-assertions.mjs";
-import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js";
+import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderMetadata.js";
import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js";
import { REA_WORKFLOW_PROVIDER } from "../dist/domain/investigationProviders.js";
import { loadRealHopperFixtureTargets } from "./lib/real-hopper-fixture.mjs";
diff --git a/src/adb/AdbProvider.ts b/src/adb/AdbProvider.ts
index 904f78db07..36724a187a 100644
--- a/src/adb/AdbProvider.ts
+++ b/src/adb/AdbProvider.ts
@@ -1,3 +1,4 @@
+import { ADB_PROVIDER_IDENTITY } from "./AdbProviderIdentity.js";
import {
absoluteCallerPath,
appendCallerPath,
@@ -152,13 +153,6 @@ import {
type AdbCommandContext,
} from "./AdbDeviceOperations.js";
-/** Stable identity for the ADB device-acquisition provider. */
-export const ADB_PROVIDER_IDENTITY = {
- id: "adb",
- name: "Android Debug Bridge",
- version: null,
-} as const;
-
const SHELL_TIMEOUT_MS = 30_000;
const PACKAGE_LIST_TIMEOUT_MS = 60_000;
const PACKAGE_LIST_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
diff --git a/src/adb/AdbProviderIdentity.ts b/src/adb/AdbProviderIdentity.ts
new file mode 100644
index 0000000000..0e0281b56b
--- /dev/null
+++ b/src/adb/AdbProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Stable identity for the ADB device-acquisition provider. */
+export const ADB_PROVIDER_IDENTITY = {
+ id: "adb",
+ name: "Android Debug Bridge",
+ version: null,
+} as const;
diff --git a/src/apktool/ApktoolProvider.ts b/src/apktool/ApktoolProvider.ts
index 8b935d4857..0bb7ef28fc 100644
--- a/src/apktool/ApktoolProvider.ts
+++ b/src/apktool/ApktoolProvider.ts
@@ -1,3 +1,4 @@
+import { APKTOOL_PROVIDER_IDENTITY } from "./ApktoolProviderIdentity.js";
import { countDecodedFiles } from "./ApktoolDecodedInventory.js";
import { ApktoolWorkspace } from "./ApktoolWorkspace.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
@@ -70,13 +71,6 @@ import {
parseManifestPackage,
} from "./ApktoolDecodeOutput.js";
-/** Stable identity for the Apktool resource-decoding provider. */
-export const APKTOOL_PROVIDER_IDENTITY = {
- id: "apktool",
- name: "Apktool",
- version: null,
-} as const;
-
const DECODE_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
const MANIFEST_MAX_BYTES = 512 * 1024;
const MAX_DECODED_ENTRIES_VISITED = 100_000;
diff --git a/src/apktool/ApktoolProviderIdentity.ts b/src/apktool/ApktoolProviderIdentity.ts
new file mode 100644
index 0000000000..c19311e4b7
--- /dev/null
+++ b/src/apktool/ApktoolProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Stable identity for the Apktool resource-decoding provider. */
+export const APKTOOL_PROVIDER_IDENTITY = {
+ id: "apktool",
+ name: "Apktool",
+ version: null,
+} as const;
diff --git a/src/application/CapabilityAvailability.ts b/src/application/CapabilityAvailability.ts
new file mode 100644
index 0000000000..73659b9762
--- /dev/null
+++ b/src/application/CapabilityAvailability.ts
@@ -0,0 +1,50 @@
+import type { ProviderAvailability } from "./AnalysisProvider.js";
+import type { JebAnalysisAvailability } from "./jeb/JebAnalysisPort.js";
+import type { AvailabilityPolicy } from "./CapabilityInventory.js";
+import type { OptionalProviderLoadFailures } from "./OptionalObservationProviders.js";
+
+export interface CapabilityAvailabilityFacts {
+ readonly probes?: Partial;
+ readonly observeProviderLoadFailures?: () => OptionalProviderLoadFailures;
+ readonly optionalProviderLoadFailures?:
+ | OptionalProviderLoadFailures
+ | undefined;
+ readonly hostUnsupported?: AvailabilityPolicy["hostUnsupported"];
+ readonly optionalFeatures?: Pick<
+ AvailabilityPolicy,
+ | "packetCaptureEnabled"
+ | "signalRecordingEnabled"
+ | "wasmArtifactEnabled"
+ | "evmInterfaceEnabled"
+ | "browserObservationEnabled"
+ | "browserScenarioEnabled"
+ | "electronObservationEnabled"
+ | "electronAutomationEnabled"
+ | "v8InspectorObservationEnabled"
+ | "javascriptRecoveryEnabled"
+ | "webModuleResolutionEnabled"
+ | "binaryLayoutEnabled"
+ | "recordedCrashEnabled"
+ | "firmwareInspectionEnabled"
+ | "firmwareExtractionEnabled"
+ >;
+}
+
+/** Provider-owned live probes used by session discovery. */
+export interface CapabilityAvailabilityProbes {
+ readonly androidAnalysisAvailability: (
+ signal: AbortSignal,
+ ) => Promise;
+ readonly adbAnalysisAvailability?: (
+ signal: AbortSignal,
+ ) => Promise;
+ readonly apktoolAnalysisAvailability?: (
+ signal: AbortSignal,
+ ) => Promise;
+ readonly jebAnalysisAvailability?: (
+ signal: AbortSignal,
+ ) => Promise;
+ readonly evmLimiterAvailability?: (
+ signal: AbortSignal,
+ ) => Promise;
+}
diff --git a/src/application/CapabilityScope.test.ts b/src/application/CapabilityScope.test.ts
new file mode 100644
index 0000000000..4842d8fafe
--- /dev/null
+++ b/src/application/CapabilityScope.test.ts
@@ -0,0 +1,116 @@
+import { expect, it, vi } from "vitest";
+
+import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
+import { CapabilityScope } from "./CapabilityScope.js";
+
+it("retains each failed cleanup while releasing successful peers across retries", async () => {
+ const scope = new CapabilityScope("Controlled mixed capability cleanup");
+ const synchronousFailure = new Error("Synchronous close refusal");
+ const asynchronousFailure = new Error("Asynchronous close refusal");
+ let allowSynchronous = false;
+ let allowAsynchronous = false;
+ const successful = vi.fn(async () => undefined);
+ const synchronous = vi.fn(() => {
+ if (!allowSynchronous) throw synchronousFailure;
+ return Promise.resolve();
+ });
+ const asynchronous = vi.fn(async () => {
+ if (!allowAsynchronous) throw asynchronousFailure;
+ });
+ const rejectedWithoutReason = vi.fn(() =>
+ allowAsynchronous ? Promise.resolve() : Promise.reject(undefined),
+ );
+ scope.own("released-peer", {}, successful);
+ scope.own("synchronous-owner", {}, synchronous);
+ scope.own("asynchronous-owner", {}, asynchronous);
+ scope.own("undefined-rejection-owner", {}, rejectedWithoutReason);
+ try {
+ const first: unknown = await scope.close().catch((cause: unknown) => cause);
+ expect(first).toBeInstanceOf(AggregateError);
+ if (!(first instanceof AggregateError))
+ throw new Error("Expected all cleanup failures");
+ expect(first.message).toBe("Controlled mixed capability cleanup");
+ expect(first.errors).toEqual([
+ synchronousFailure,
+ asynchronousFailure,
+ undefined,
+ ]);
+ for (const close of [
+ successful,
+ synchronous,
+ asynchronous,
+ rejectedWithoutReason,
+ ])
+ expect(close).toHaveBeenCalledTimes(1);
+ expect(() => scope.own("late-owner", {}, successful)).toThrow(
+ "after host shutdown",
+ );
+
+ allowAsynchronous = true;
+ const second: unknown = await scope
+ .close()
+ .catch((cause: unknown) => cause);
+ expect(second).toBeInstanceOf(AggregateError);
+ if (!(second instanceof AggregateError))
+ throw new Error("Expected the remaining owner failure");
+ expect(second.errors).toEqual([synchronousFailure]);
+ expect(successful).toHaveBeenCalledTimes(1);
+ expect(synchronous).toHaveBeenCalledTimes(2);
+ expect(asynchronous).toHaveBeenCalledTimes(2);
+ expect(rejectedWithoutReason).toHaveBeenCalledTimes(2);
+
+ allowSynchronous = true;
+ await scope.close();
+ await scope.close();
+ expect(synchronous).toHaveBeenCalledTimes(3);
+ expect(successful).toHaveBeenCalledTimes(1);
+ expect(asynchronous).toHaveBeenCalledTimes(2);
+ expect(rejectedWithoutReason).toHaveBeenCalledTimes(2);
+ } finally {
+ allowSynchronous = true;
+ allowAsynchronous = true;
+ await scope.close();
+ }
+});
+
+it("joins concurrent shutdowns and seals ownership before asynchronous cleanup settles", async () => {
+ const scope = new CapabilityScope();
+ let acknowledge = () => {};
+ const entered = new Promise((resolve) => {
+ acknowledge = resolve;
+ });
+ let release = () => {};
+ const released = new Promise((resolve) => {
+ release = resolve;
+ });
+ const close = vi.fn(async () => {
+ acknowledge();
+ await released;
+ });
+ scope.own("controlled-owner", {}, close);
+ const first = scope.close();
+ let second: Promise | undefined;
+ try {
+ await awaitAcknowledgement(entered, first, "capability cleanup");
+ second = scope.close();
+ expect(second).toBe(first);
+ expect(close).toHaveBeenCalledTimes(1);
+ expect(() => scope.own("later-owner", {}, async () => undefined)).toThrow(
+ "after host shutdown",
+ );
+ release();
+ await Promise.all([first, second]);
+ expect(() =>
+ scope.own("post-shutdown-owner", {}, async () => undefined),
+ ).toThrow("after host shutdown");
+ await scope.close();
+ expect(close).toHaveBeenCalledTimes(1);
+ } finally {
+ release();
+ await Promise.allSettled([
+ first,
+ ...(second === undefined ? [] : [second]),
+ ]);
+ await scope.close();
+ }
+});
diff --git a/src/application/CapabilityScope.ts b/src/application/CapabilityScope.ts
new file mode 100644
index 0000000000..68dc4ea198
--- /dev/null
+++ b/src/application/CapabilityScope.ts
@@ -0,0 +1,40 @@
+/** One host owns these exact close capabilities; a released peer stays released. */
+export class CapabilityScope {
+ readonly #pending = new Map Promise>();
+ #accepting = true;
+ #closing: Promise | undefined;
+
+ constructor(readonly failureMessage = "REA provider cleanup failed") {}
+
+ own(id: string, value: Value, close: () => Promise): Value {
+ if (!this.#accepting)
+ throw new Error(`Cannot acquire capability ${id} after host shutdown`);
+ if (this.#pending.has(id))
+ throw new Error(`Capability ${id} already belongs to this host`);
+ this.#pending.set(id, close);
+ return value;
+ }
+
+ /** Join concurrent closes, retaining only failed owners for the next retry. */
+ close(): Promise {
+ this.#accepting = false;
+ this.#closing ??= Promise.allSettled(
+ [...this.#pending].map(async ([id, close]) => {
+ // A synchronous adapter throw must not prevent other owners' cleanup.
+ await Promise.resolve().then(close);
+ this.#pending.delete(id);
+ }),
+ )
+ .then((results) => {
+ const failures = results.flatMap((result) =>
+ result.status === "rejected" ? [result.reason] : [],
+ );
+ if (failures.length > 0)
+ throw new AggregateError(failures, this.failureMessage);
+ })
+ .finally(() => {
+ this.#closing = undefined;
+ });
+ return this.#closing;
+ }
+}
diff --git a/src/application/WebModuleTraceService.ts b/src/application/WebModuleTraceService.ts
index ae8f599ab0..090ec95b30 100644
--- a/src/application/WebModuleTraceService.ts
+++ b/src/application/WebModuleTraceService.ts
@@ -1,3 +1,4 @@
+import { WEB_MODULE_TRACE_PROVIDER } from "../domain/webTraceProviders.js";
import { closeOwnedWebResources } from "./closeOwnedWebResources.js";
import { isDeepStrictEqual } from "node:util";
import { z } from "zod";
@@ -273,11 +274,7 @@ export class WebModuleTraceService {
sha256: data.sourceFile.sha256,
format: "file",
},
- {
- id: "rea-web-module-trace",
- name: "REA captured module relationship trace",
- version: "1",
- },
+ WEB_MODULE_TRACE_PROVIDER,
{
operation: OPERATION,
parameters: input,
diff --git a/src/application/WebSourceLocationService.ts b/src/application/WebSourceLocationService.ts
index 90244172d0..a818eb9316 100644
--- a/src/application/WebSourceLocationService.ts
+++ b/src/application/WebSourceLocationService.ts
@@ -1,3 +1,4 @@
+import { WEB_SOURCE_LOCATION_PROVIDER } from "../domain/webTraceProviders.js";
import { closeOwnedWebResources } from "./closeOwnedWebResources.js";
import { createHash } from "node:crypto";
import { z } from "zod";
@@ -209,11 +210,7 @@ export class WebSourceLocationService {
sha256: data.sourceFile.sha256,
format: "file",
},
- {
- id: "rea-web-source-location",
- name: "REA captured source-map point trace",
- version: "1",
- },
+ WEB_SOURCE_LOCATION_PROVIDER,
{
operation: OPERATION,
parameters: input,
diff --git a/src/application/binary/BinarySessionStatus.test.ts b/src/application/binary/BinarySessionStatus.test.ts
index d0a714d83a..c1d97d8eda 100644
--- a/src/application/binary/BinarySessionStatus.test.ts
+++ b/src/application/binary/BinarySessionStatus.test.ts
@@ -25,9 +25,9 @@ import {
} from "./BinarySessionStatus.js";
import {
APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE,
- NATIVE_MACOS_PROVIDER_IDENTITY,
nativeMacOSCapabilities,
} from "../../native/NativeMacOSProviderMetadata.js";
+import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../../native/NativeMacosProviderIdentity.js";
const projectedStatusSchema = z.object({
capabilities: sessionProviderFacts.shape.capabilities,
diff --git a/src/application/erlang/BeamService.test.ts b/src/application/erlang/BeamService.test.ts
index 7df93684ae..88a5221490 100644
--- a/src/application/erlang/BeamService.test.ts
+++ b/src/application/erlang/BeamService.test.ts
@@ -1,6 +1,6 @@
import { expect, it } from "vitest";
import { BeamService } from "./BeamService.js";
-import { ERLANG_BEAM_PROVIDER_IDENTITY } from "../../erlang/BeamProvider.js";
+import { ERLANG_BEAM_PROVIDER_IDENTITY } from "../../erlang/ErlangBeamProviderIdentity.js";
import type { ErlangBeam } from "../../domain/erlang/beam.js";
import { ok } from "../../domain/result.js";
import {
diff --git a/src/application/sqlite/SqliteDatabaseCapability.ts b/src/application/sqlite/SqliteDatabaseCapability.ts
index f3c0308676..fe3283bee3 100644
--- a/src/application/sqlite/SqliteDatabaseCapability.ts
+++ b/src/application/sqlite/SqliteDatabaseCapability.ts
@@ -1,6 +1,6 @@
import { satisfies } from "semver";
import { toolContract } from "../../contracts/toolContracts.js";
-import { SQLITE_PROVIDER_IDENTITY } from "../../domain/sqlite/sqliteDatabase.js";
+import { SQLITE_PROVIDER_IDENTITY } from "../../domain/sqlite/SqliteProviderIdentity.js";
const SQLITE_RUNTIME_REQUIREMENT =
"SQLite inspection requires Node.js 24.x >=24.15 or 26+ with the native DatabaseSync.limits API. Other REA features retain their published runtime requirements. The inspection worker verifies the API before querying the snapshot.";
diff --git a/src/browser/history/CaptureProviderIdentities.ts b/src/browser/history/CaptureProviderIdentities.ts
new file mode 100644
index 0000000000..2dd17e9f3e
--- /dev/null
+++ b/src/browser/history/CaptureProviderIdentities.ts
@@ -0,0 +1,28 @@
+/** Upstream packages and owned schema corrections for offline HAR decoding. */
+export const HAR_CAPTURE_PROVIDER_IDENTITY = {
+ id: "har-schema/lossless-json",
+ name: "REA HAR capture adapter",
+ version:
+ "har-schema@2.0.0;har-schema-corrections@1;jsonc-parser@3.3.1;lossless-json@4.3.1",
+} as const;
+
+/** Native SAZ member decoding with the existing ZIP and XML package profiles. */
+export const SAZ_CAPTURE_PROVIDER_IDENTITY = {
+ id: "fiddler-saz",
+ name: "REA Fiddler SAZ capture adapter",
+ version: "1;zip.js@2.15.0;xmldom@0.9.12",
+} as const;
+
+/** Native executable profile verified with unchanged mitmproxy raw-state decoding. */
+export const MITMPROXY_CAPTURE_PROVIDER_IDENTITY = {
+ id: "mitmproxy-native-tnetstring",
+ name: "REA offline native mitmproxy adapter",
+ version: "12.2.3",
+} as const;
+
+export const QLOG_CAPTURE_PROVIDER_IDENTITY = {
+ id: "aioquic-qlog-json",
+ name: "REA aioquic qlog capture adapter",
+ version:
+ "aioquic-profile@1.5.0;qlog@0.3;jsonc-parser@3.3.1;lossless-json@4.3.1",
+} as const;
diff --git a/src/browser/history/CaptureRelease.ts b/src/browser/history/CaptureRelease.ts
index b02ec806f5..2702622e0e 100644
--- a/src/browser/history/CaptureRelease.ts
+++ b/src/browser/history/CaptureRelease.ts
@@ -5,14 +5,6 @@ import {
import { javaScriptRuntime } from "../../domain/runtimeVersion.js";
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
-/** Upstream packages and owned schema corrections for offline HAR decoding. */
-export const HAR_CAPTURE_PROVIDER_IDENTITY = {
- id: "har-schema/lossless-json",
- name: "REA HAR capture adapter",
- version:
- "har-schema@2.0.0;har-schema-corrections@1;jsonc-parser@3.3.1;lossless-json@4.3.1",
-} as const;
-
/**
* Default V8 heap settings for the owned Node capture decoder process.
* The old generation is `REA_CAPTURE_DECODER_OLD_GENERATION_MIB`; the
@@ -38,24 +30,4 @@ export const NODE_CAPTURE_PROCESS_MEMORY_LIMIT_BYTES =
NODE_CAPTURE_HEAP_LIMITS.oldGenerationMiB,
);
-/** Native executable profile verified with unchanged mitmproxy raw-state decoding. */
-export const MITMPROXY_CAPTURE_PROVIDER_IDENTITY = {
- id: "mitmproxy-native-tnetstring",
- name: "REA offline native mitmproxy adapter",
- version: "12.2.3",
-} as const;
-
-/** Native SAZ member decoding with the existing ZIP and XML package profiles. */
-export const SAZ_CAPTURE_PROVIDER_IDENTITY = {
- id: "fiddler-saz",
- name: "REA Fiddler SAZ capture adapter",
- version: "1;zip.js@2.15.0;xmldom@0.9.12",
-} as const;
-
/** Fixed offline interpretation profile, not an authenticated producer claim. */
-export const QLOG_CAPTURE_PROVIDER_IDENTITY = {
- id: "aioquic-qlog-json",
- name: "REA aioquic qlog capture adapter",
- version:
- "aioquic-profile@1.5.0;qlog@0.3;jsonc-parser@3.3.1;lossless-json@4.3.1",
-} as const;
diff --git a/src/browser/history/HarCapture.ts b/src/browser/history/HarCapture.ts
index ddc2459118..91c67f9efe 100644
--- a/src/browser/history/HarCapture.ts
+++ b/src/browser/history/HarCapture.ts
@@ -15,7 +15,7 @@ import {
capturePointer,
} from "./CaptureRedaction.js";
import { createHarValidator } from "./HarSchema.js";
-import { HAR_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js";
+import { HAR_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js";
type DecodedCapture = Omit<
WebNetworkCapture,
diff --git a/src/browser/history/MitmproxyCaptureAdapter.ts b/src/browser/history/MitmproxyCaptureAdapter.ts
index 51597d7c33..cd2bab3023 100644
--- a/src/browser/history/MitmproxyCaptureAdapter.ts
+++ b/src/browser/history/MitmproxyCaptureAdapter.ts
@@ -4,7 +4,7 @@ import { isAbsolute } from "node:path";
import { fileURLToPath } from "node:url";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import type { HistoricalCaptureFormatAdapter } from "./HistoricalCaptureFormatAdapter.js";
-import { MITMPROXY_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js";
+import { MITMPROXY_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js";
import { snapshotEnvironment } from "../../process/snapshotEnvironment.js";
const OPERATION = "inspect_web_network_capture";
const MITMDUMP_REQUIREMENT =
diff --git a/src/browser/history/NodeCaptureAdapter.ts b/src/browser/history/NodeCaptureAdapter.ts
index 3e51bd0089..785b157220 100644
--- a/src/browser/history/NodeCaptureAdapter.ts
+++ b/src/browser/history/NodeCaptureAdapter.ts
@@ -8,11 +8,13 @@ import {
import { CAPTURE_DECODER_SEMI_SPACE_MIB } from "../../domain/analysisResourceLimits.js";
import {
NODE_CAPTURE_HEAP_ENFORCED,
- HAR_CAPTURE_PROVIDER_IDENTITY,
- SAZ_CAPTURE_PROVIDER_IDENTITY,
- QLOG_CAPTURE_PROVIDER_IDENTITY,
captureDecoderProcessMemoryLimitBytes,
} from "./CaptureRelease.js";
+import { QLOG_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js";
+import {
+ HAR_CAPTURE_PROVIDER_IDENTITY,
+ SAZ_CAPTURE_PROVIDER_IDENTITY,
+} from "./CaptureProviderIdentities.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
/** Package-backed capture decoders share one bounded offline Node process profile. */
diff --git a/src/browser/history/QlogCapture.ts b/src/browser/history/QlogCapture.ts
index 61a22d9ee7..61f09804b0 100644
--- a/src/browser/history/QlogCapture.ts
+++ b/src/browser/history/QlogCapture.ts
@@ -14,7 +14,7 @@ import {
capturePointer,
isCaptureCredentialHeader,
} from "./CaptureRedaction.js";
-import { QLOG_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js";
+import { QLOG_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js";
const MAX_VARINT = (1n << 62n) - 1n;
type Projection = Pick<
diff --git a/src/browser/history/SazCapture.ts b/src/browser/history/SazCapture.ts
index abb7f7caa2..a64ad3e2dc 100644
--- a/src/browser/history/SazCapture.ts
+++ b/src/browser/history/SazCapture.ts
@@ -7,7 +7,7 @@ import {
WEB_NETWORK_CAPTURE_LIMITS,
type WebNetworkCaptureRecord,
} from "../../domain/webNetworkCapture.js";
-import { SAZ_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js";
+import { SAZ_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js";
import { CaptureFormatError } from "./CaptureFormatError.js";
import { decodeSazHttp } from "./SazHttp.js";
import { decodeSazMetadata } from "./SazMetadata.js";
diff --git a/src/captures/TsharkCaptureProvider.ts b/src/captures/TsharkCaptureProvider.ts
index 55dadaa8d4..a66b2571c0 100644
--- a/src/captures/TsharkCaptureProvider.ts
+++ b/src/captures/TsharkCaptureProvider.ts
@@ -1,3 +1,4 @@
+import { TSHARK_PROVIDER_IDENTITY } from "./TsharkProviderIdentity.js";
import { artifactFileCancellationCause } from "../artifacts/ArtifactFileOperation.js";
import {
type InspectPacketFieldsInput,
@@ -71,11 +72,7 @@ import {
import { privateWorkspaceFailure } from "../process/privateWorkspaceFailure.js";
const OPERATION = "inspect_packet_capture";
-export const TSHARK_PROVIDER_IDENTITY = {
- id: "tshark-capture",
- name: "REA TShark capture adapter",
- version: "tshark@4",
-} as const;
+
type Launcher = NonNullable[2]>["launcher"];
/** Packet inspection runs only on Linux; discovery and execution share this check. */
diff --git a/src/captures/TsharkProviderIdentity.ts b/src/captures/TsharkProviderIdentity.ts
new file mode 100644
index 0000000000..66e61b4748
--- /dev/null
+++ b/src/captures/TsharkProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const TSHARK_PROVIDER_IDENTITY = {
+ id: "tshark-capture",
+ name: "REA TShark capture adapter",
+ version: "tshark@4",
+} as const;
diff --git a/src/cli/cutterCommands.ts b/src/cli/cutterCommands.ts
index 40457eaa05..258175c4dd 100644
--- a/src/cli/cutterCommands.ts
+++ b/src/cli/cutterCommands.ts
@@ -2,8 +2,7 @@ import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
import { z } from "incur";
import type { Logger } from "pino";
-import { CutterBridgeClient } from "../cutter/CutterBridgeClient.js";
-import { CutterBridgeService } from "../cutter/CutterBridgeService.js";
+import { createCutterBridgeService } from "../composition/cutter.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
@@ -17,8 +16,7 @@ export const registerCutterCommands = (
logger: Logger,
environment: Readonly,
): void => {
- const createCutter = () =>
- new CutterBridgeService(new CutterBridgeClient(environment));
+ const createCutter = () => createCutterBridgeService(environment);
cli.command(CLI_COMMANDS.listCutterSessions, {
description:
diff --git a/src/cli/fridaCommands.ts b/src/cli/fridaCommands.ts
index 2475e5b85a..485f28c9e5 100644
--- a/src/cli/fridaCommands.ts
+++ b/src/cli/fridaCommands.ts
@@ -6,7 +6,7 @@ import type {
FridaInstrumentationPort,
FridaRemoteConnection,
} from "../application/frida/FridaInstrumentationPort.js";
-import { FridaInstrumentationManager } from "../frida/FridaInstrumentationManager.js";
+import { createFridaInstrumentationProvider } from "../composition/frida.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
@@ -27,7 +27,7 @@ const exactlyOneScriptSource =
export const registerFridaCommands = (
cli: CliInstance,
logger: Logger,
- provider: FridaInstrumentationPort = new FridaInstrumentationManager(),
+ provider: FridaInstrumentationPort = createFridaInstrumentationProvider(),
): void => {
const service = new FridaInstrumentationService(provider);
// CLI calls own a disposable provider; MCP retains its shared provider until
diff --git a/src/cli/reverseEngineeringCommands.ts b/src/cli/reverseEngineeringCommands.ts
index 87d9ff95c1..b07da0176b 100644
--- a/src/cli/reverseEngineeringCommands.ts
+++ b/src/cli/reverseEngineeringCommands.ts
@@ -22,11 +22,11 @@ import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import type { ReverseEngineeringService } from "../application/reverse/ReverseEngineeringService.js";
+import { gdbOneShotCommandMayInvalidateDetachProof } from "../gdb/GdbSessionManager.js";
import {
- GdbSessionManager,
- gdbOneShotCommandMayInvalidateDetachProof,
-} from "../gdb/GdbSessionManager.js";
-import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js";
+ createGdbSessionManager,
+ createRizinDebugSessionManager,
+} from "../composition/debuggerSessions.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import {
analysisEvidenceDeliveryOptions,
@@ -171,8 +171,7 @@ export const registerReverseEngineeringCommands = (
environment: Readonly,
createGdbManager: (
environment: Readonly,
- ) => GdbSessionManager = (providerEnvironment) =>
- new GdbSessionManager({ environment: providerEnvironment }),
+ ) => ReturnType = createGdbSessionManager,
createService: typeof createReverseEngineeringService = createReverseEngineeringService,
): void => {
const service = createService(environment);
@@ -417,7 +416,7 @@ export const registerReverseEngineeringCommands = (
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.debugWithRizin, async () => {
- const manager = new RizinDebugSessionManager(environment);
+ const manager = createRizinDebugSessionManager(environment);
let sessionId: string | undefined;
return finishAnalysisCommand(
async () => {
diff --git a/src/composition/auxiliaryAnalysisProviders.ts b/src/composition/auxiliaryAnalysisProviders.ts
index 2be36f8f49..ed58abcac2 100644
--- a/src/composition/auxiliaryAnalysisProviders.ts
+++ b/src/composition/auxiliaryAnalysisProviders.ts
@@ -7,10 +7,8 @@ import {
MANAGED_STATIC_PROVIDER,
} from "../domain/investigationProviders.js";
import { managedStaticCapabilities } from "../dotnet/ManagedStaticProviderMetadata.js";
-import {
- NATIVE_MACOS_PROVIDER_IDENTITY,
- nativeMacOSCapabilities,
-} from "../native/NativeMacOSProviderMetadata.js";
+import { nativeMacOSCapabilities } from "../native/NativeMacOSProviderMetadata.js";
+import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../native/NativeMacosProviderIdentity.js";
type AuxiliaryProviderDeclaration = ConstructorParameters<
typeof LazyAnalysisProvider
diff --git a/src/composition/capabilityDeclarations.ts b/src/composition/capabilityDeclarations.ts
new file mode 100644
index 0000000000..cfbfd494d7
--- /dev/null
+++ b/src/composition/capabilityDeclarations.ts
@@ -0,0 +1,530 @@
+// oxlint-disable max-lines -- Keep the exhaustive static provenance bindings together; this table has no execution or host-availability logic.
+import type { ProviderIdentity } from "../application/AnalysisProvider.js";
+import type { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
+import type { ToolContractGroupId } from "../contracts/toolFamilies.js";
+import { ADB_PROVIDER_IDENTITY } from "../adb/AdbProviderIdentity.js";
+import { JADX_PROVIDER_IDENTITY } from "../android/JadxRelease.js";
+import { APK_SIGNATURE_PROVIDER_IDENTITY } from "../apkSignature/ApkSignatureRelease.js";
+import { APKTOOL_PROVIDER_IDENTITY } from "../apktool/ApktoolProviderIdentity.js";
+import {
+ HAR_CAPTURE_PROVIDER_IDENTITY,
+ MITMPROXY_CAPTURE_PROVIDER_IDENTITY,
+ QLOG_CAPTURE_PROVIDER_IDENTITY,
+ SAZ_CAPTURE_PROVIDER_IDENTITY,
+} from "../browser/history/CaptureProviderIdentities.js";
+import {
+ CDP_BROWSER_PROVIDER_IDENTITY,
+ CDP_ELECTRON_PROVIDER_IDENTITY,
+ PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
+ PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
+} from "../browser/providerIdentities.js";
+import { TSHARK_PROVIDER_IDENTITY } from "../captures/TsharkProviderIdentity.js";
+import { CUTTER_PROVIDER_IDENTITY } from "../cutter/CutterProviderIdentity.js";
+import {
+ ANALYSIS_VIEW_PROVIDER,
+ ANDROID_APPLICATION_PROVIDER,
+ APPLE_APPLICATION_PROVIDER,
+ APPLE_IPS_PROVIDER,
+ ARTIFACT_GRAPH_PROVIDER,
+ BYTE_ENTROPY_PROVIDER,
+ DEVICETREE_PROVIDER,
+ ELF_NOTES_PROVIDER,
+ HARMONY_APPLICATION_PROVIDER,
+ JAVASCRIPT_APPLICATION_PROVIDER,
+ JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER,
+ JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER,
+ KERNEL_MODULE_PROVIDER,
+ MANAGED_STATIC_PROVIDER,
+ MANAGED_WORKFLOW_PROVIDER,
+ NATIVE_EXPORTS_PROVIDER,
+ PE_CERTIFICATES_PROVIDER,
+ PE_IMPORTS_PROVIDER,
+ PE_LOAD_CONFIG_PROVIDER,
+ PE_RESOURCES_PROVIDER,
+ WEB_SCRIPT_EXPORT_PROVIDER,
+} from "../domain/investigationProviders.js";
+import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/SqliteProviderIdentity.js";
+import {
+ WEB_MODULE_TRACE_PROVIDER,
+ WEB_SOURCE_LOCATION_PROVIDER,
+} from "../domain/webTraceProviders.js";
+import { ELECTRON_FUSE_PROVIDER_IDENTITY } from "../electron/ElectronFuseProviderIdentity.js";
+import { ERLANG_BEAM_PROVIDER_IDENTITY } from "../erlang/ErlangBeamProviderIdentity.js";
+import { EVMOLE_PROVIDER_IDENTITY } from "../evm/EvmoleProviderIdentity.js";
+import {
+ BINWALK_PROVIDER_IDENTITY,
+ UNBLOB_PROVIDER_IDENTITY,
+} from "../firmware/FirmwareProviderIdentities.js";
+import { FLUTTER_PROVIDER_IDENTITY } from "../flutter/FlutterProviderIdentity.js";
+import { FRIDA_PROVIDER_IDENTITY } from "../frida/FridaProviderIdentity.js";
+import { GDB_PROVIDER_IDENTITY } from "../gdb/GdbProviderIdentity.js";
+import {
+ GHIDRA_OPERATIONS,
+ GHIDRA_PROVIDER_IDENTITY,
+} from "../ghidra/GhidraProviderMetadata.js";
+import { GO_BINARY_PROVIDER_IDENTITY } from "../go/GoBinaryProviderIdentity.js";
+import { GPU_CODE_OBJECTS_PROVIDER_IDENTITY } from "../gpu/GpuCodeObjectsProviderIdentity.js";
+import {
+ HOPPER_OPERATIONS,
+ HOPPER_PROVIDER_IDENTITY,
+} from "../hopper/HopperProviderMetadata.js";
+import {
+ IDA_OPERATIONS,
+ IDA_PROVIDER_IDENTITY,
+} from "../ida/IdaProviderMetadata.js";
+import { IL2CPP_METADATA_PROVIDER_IDENTITY } from "../il2cpp/Il2CppMetadataProviderIdentity.js";
+import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../inspector/providerIdentity.js";
+import { WAKARU_PROVIDER_IDENTITY } from "../javascript/recovery/WakaruProviderIdentity.js";
+import { JEB_PROVIDER_IDENTITY } from "../jeb/JebProviderIdentity.js";
+import {
+ KUNA_OPERATIONS,
+ KUNA_PROVIDER_IDENTITY,
+} from "../kuna/KunaProviderMetadata.js";
+import { MODEL_ARTIFACT_PROVIDER_IDENTITY } from "../model/ModelArtifactProviderIdentity.js";
+import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../native/NativeMacosProviderIdentity.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "../native/pwntools/PwntoolsProviderIdentity.js";
+import { OBJDUMP_PROVIDER_IDENTITY } from "../objdump/ObjdumpProviderIdentity.js";
+import { PCAP_PROVIDER_IDENTITY } from "../pcap/PcapProviderIdentity.js";
+import { PYTHON_PYC_PROVIDER_IDENTITY } from "../python/PythonPycProviderIdentity.js";
+import { RIZIN_DEBUGGER_PROVIDER_IDENTITY } from "../rizin/RizinDebuggerProviderIdentity.js";
+import { RIZIN_PROVIDER_IDENTITY } from "../rizin/RizinProviderIdentity.js";
+import { RUST_BINARY_PROVIDER_IDENTITY } from "../rust/RustBinaryProviderIdentity.js";
+import { SIGROK_PROVIDER_IDENTITY } from "../signals/SigrokProviderIdentity.js";
+import { WABT_PROVIDER_IDENTITY } from "../wasm/WabtRelease.js";
+
+type CanonicalOperationName = (typeof TOOL_CONTRACTS)[number]["name"];
+
+/** Whole source groups grow with their canonical contracts; provider subsets stay explicit. */
+export type CapabilityOperationSelection =
+ | readonly CanonicalOperationName[]
+ | { readonly group: ToolContractGroupId };
+
+/** Static provenance bindings; host availability belongs to runtime inspection. */
+export interface CapabilityDeclaration {
+ readonly providers: readonly {
+ readonly identity: ProviderIdentity;
+ readonly operations: CapabilityOperationSelection;
+ }[];
+}
+
+const providerBinding = (
+ identity: ProviderIdentity,
+ operations: Operations,
+) => ({ identity, operations });
+
+/** Shared source facts, with no factory, schema or generated-catalog imports. */
+export const CAPABILITY_DECLARATIONS = {
+ adbDevice: {
+ providers: [
+ providerBinding(ADB_PROVIDER_IDENTITY, { group: "ADB_TOOL_CONTRACTS" }),
+ ],
+ },
+ analysisView: {
+ providers: [
+ providerBinding(ANALYSIS_VIEW_PROVIDER, {
+ group: "ANALYSIS_VIEW_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ android: {
+ providers: [
+ providerBinding(JADX_PROVIDER_IDENTITY, {
+ group: "ANDROID_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ androidApplicationGraph: {
+ providers: [
+ providerBinding(ANDROID_APPLICATION_PROVIDER, [
+ "project_android_application_graph",
+ ]),
+ ],
+ },
+ apkSignature: {
+ providers: [
+ providerBinding(APK_SIGNATURE_PROVIDER_IDENTITY, {
+ group: "APK_SIGNATURE_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ apktool: {
+ providers: [
+ providerBinding(APKTOOL_PROVIDER_IDENTITY, {
+ group: "APKTOOL_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ appleApplicationGraph: {
+ providers: [
+ providerBinding(APPLE_APPLICATION_PROVIDER, [
+ "project_apple_application_graph",
+ ]),
+ ],
+ },
+ appleIps: {
+ providers: [providerBinding(APPLE_IPS_PROVIDER, ["inspect_apple_ips"])],
+ },
+ applicationWorkflows: {
+ providers: [
+ providerBinding(JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, [
+ "build_reconstruction_obligation_ledger",
+ "compare_application_versions",
+ "compare_javascript_export_shapes",
+ "compare_source_to_bundle",
+ "evaluate_reconstruction_coverage",
+ "guided_workflows",
+ "read_guidance_section",
+ "search_guidance",
+ "trace_application_feature",
+ "trace_javascript_semantics",
+ ]),
+ ],
+ },
+ artifactGraph: {
+ providers: [
+ providerBinding(ARTIFACT_GRAPH_PROVIDER, {
+ group: "ARTIFACT_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ artifactResources: { providers: [] },
+ binaryLayout: {
+ providers: [
+ providerBinding(PWNTOOLS_PROVIDER_IDENTITY, ["inspect_binary_layout"]),
+ ],
+ },
+ browserObservation: {
+ providers: [
+ providerBinding(CDP_BROWSER_PROVIDER_IDENTITY, {
+ group: "BROWSER_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ browserScenarioCapture: {
+ providers: [
+ providerBinding(PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY, {
+ group: "BROWSER_SCENARIO_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ byteEntropy: {
+ providers: [
+ providerBinding(BYTE_ENTROPY_PROVIDER, ["inspect_byte_entropy"]),
+ ],
+ },
+ cutter: {
+ providers: [
+ providerBinding(CUTTER_PROVIDER_IDENTITY, {
+ group: "CUTTER_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ desktopApplication: {
+ providers: [
+ providerBinding(ARTIFACT_GRAPH_PROVIDER, {
+ group: "DESKTOP_APPLICATION_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ devicetree: {
+ providers: [providerBinding(DEVICETREE_PROVIDER, ["inspect_devicetree"])],
+ },
+ electronActiveObservation: {
+ providers: [
+ providerBinding(PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY, [
+ "capture_electron_scenario",
+ ]),
+ ],
+ },
+ electronFuses: {
+ providers: [
+ providerBinding(ELECTRON_FUSE_PROVIDER_IDENTITY, [
+ "inspect_electron_fuses",
+ ]),
+ ],
+ },
+ electronObservation: {
+ providers: [
+ providerBinding(CDP_ELECTRON_PROVIDER_IDENTITY, [
+ "inspect_electron_page",
+ "list_electron_targets",
+ ]),
+ ],
+ },
+ elfNotes: {
+ providers: [providerBinding(ELF_NOTES_PROVIDER, ["inspect_elf_notes"])],
+ },
+ erlangBeam: {
+ providers: [
+ providerBinding(ERLANG_BEAM_PROVIDER_IDENTITY, {
+ group: "ERLANG_BEAM_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ evmInterface: {
+ providers: [
+ providerBinding(EVMOLE_PROVIDER_IDENTITY, {
+ group: "EVM_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ firmwareAnalysis: {
+ providers: [
+ providerBinding(BINWALK_PROVIDER_IDENTITY, ["inspect_firmware_regions"]),
+ providerBinding(UNBLOB_PROVIDER_IDENTITY, ["extract_firmware"]),
+ ],
+ },
+ flutter: {
+ providers: [
+ providerBinding(FLUTTER_PROVIDER_IDENTITY, {
+ group: "FLUTTER_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ fridaInstrumentation: {
+ providers: [
+ providerBinding(FRIDA_PROVIDER_IDENTITY, {
+ group: "FRIDA_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ gdbSessions: {
+ providers: [
+ providerBinding(GDB_PROVIDER_IDENTITY, { group: "GDB_TOOL_CONTRACTS" }),
+ ],
+ },
+ ghidra: {
+ providers: [providerBinding(GHIDRA_PROVIDER_IDENTITY, GHIDRA_OPERATIONS)],
+ },
+ goBinary: {
+ providers: [
+ providerBinding(GO_BINARY_PROVIDER_IDENTITY, {
+ group: "GO_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ gpuCodeObjects: {
+ providers: [
+ providerBinding(GPU_CODE_OBJECTS_PROVIDER_IDENTITY, {
+ group: "GPU_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ harmonyApplicationGraph: {
+ providers: [
+ providerBinding(HARMONY_APPLICATION_PROVIDER, [
+ "project_harmony_application_graph",
+ ]),
+ ],
+ },
+ hopper: {
+ providers: [providerBinding(HOPPER_PROVIDER_IDENTITY, HOPPER_OPERATIONS)],
+ },
+ ida: {
+ providers: [providerBinding(IDA_PROVIDER_IDENTITY, IDA_OPERATIONS)],
+ },
+ il2cppMetadata: {
+ providers: [
+ providerBinding(IL2CPP_METADATA_PROVIDER_IDENTITY, {
+ group: "IL2CPP_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ javascriptApplication: {
+ providers: [
+ providerBinding(JAVASCRIPT_APPLICATION_PROVIDER, [
+ "analyze_javascript_application",
+ ]),
+ ],
+ },
+ javascriptRecovery: {
+ providers: [
+ providerBinding(WAKARU_PROVIDER_IDENTITY, {
+ group: "JAVASCRIPT_RECOVERY_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ javascriptRuntimeObservation: {
+ providers: [
+ providerBinding(V8_INSPECTOR_PROVIDER_IDENTITY, {
+ group: "JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ javascriptRuntimeReconciliation: {
+ providers: [
+ providerBinding(JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER, [
+ "reconcile_javascript_runtime",
+ ]),
+ ],
+ },
+ jeb: {
+ providers: [
+ providerBinding(JEB_PROVIDER_IDENTITY, { group: "JEB_TOOL_CONTRACTS" }),
+ ],
+ },
+ kernelModule: {
+ providers: [
+ providerBinding(KERNEL_MODULE_PROVIDER, ["inspect_kernel_module"]),
+ ],
+ },
+ kuna: {
+ providers: [providerBinding(KUNA_PROVIDER_IDENTITY, KUNA_OPERATIONS)],
+ },
+ managedStatic: {
+ providers: [
+ providerBinding(MANAGED_STATIC_PROVIDER, {
+ group: "MANAGED_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ managedWorkflow: {
+ providers: [
+ providerBinding(MANAGED_WORKFLOW_PROVIDER, {
+ group: "MANAGED_WORKFLOW_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ modelArtifact: {
+ providers: [
+ providerBinding(MODEL_ARTIFACT_PROVIDER_IDENTITY, {
+ group: "MODEL_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ nativeExports: {
+ providers: [
+ providerBinding(NATIVE_EXPORTS_PROVIDER, ["inspect_native_exports"]),
+ ],
+ },
+ nativeMacOS: {
+ providers: [
+ providerBinding(NATIVE_MACOS_PROVIDER_IDENTITY, {
+ group: "NATIVE_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ packetCapture: {
+ providers: [
+ providerBinding(TSHARK_PROVIDER_IDENTITY, {
+ group: "CAPTURE_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ pcap: {
+ providers: [
+ providerBinding(PCAP_PROVIDER_IDENTITY, { group: "PCAP_TOOL_CONTRACTS" }),
+ ],
+ },
+ peCertificates: {
+ providers: [
+ providerBinding(PE_CERTIFICATES_PROVIDER, ["inspect_pe_certificates"]),
+ ],
+ },
+ peImports: {
+ providers: [providerBinding(PE_IMPORTS_PROVIDER, ["inspect_pe_imports"])],
+ },
+ peLoadConfig: {
+ providers: [
+ providerBinding(PE_LOAD_CONFIG_PROVIDER, ["inspect_pe_load_config"]),
+ ],
+ },
+ peResources: {
+ providers: [
+ providerBinding(PE_RESOURCES_PROVIDER, ["inspect_pe_resources"]),
+ ],
+ },
+ pythonPyc: {
+ providers: [
+ providerBinding(PYTHON_PYC_PROVIDER_IDENTITY, {
+ group: "PYTHON_PYC_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ recordedCrash: {
+ providers: [
+ providerBinding(PWNTOOLS_PROVIDER_IDENTITY, ["inspect_recorded_crash"]),
+ ],
+ },
+ reverseEngineering: {
+ providers: [
+ providerBinding(OBJDUMP_PROVIDER_IDENTITY, ["inspect_with_objdump"]),
+ providerBinding(RIZIN_PROVIDER_IDENTITY, ["execute_rizin_command"]),
+ ],
+ },
+ rizinDebugSessions: {
+ providers: [
+ providerBinding(RIZIN_DEBUGGER_PROVIDER_IDENTITY, {
+ group: "RIZIN_DEBUG_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ rustBinary: {
+ providers: [
+ providerBinding(RUST_BINARY_PROVIDER_IDENTITY, {
+ group: "RUST_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ signalRecording: {
+ providers: [
+ providerBinding(SIGROK_PROVIDER_IDENTITY, {
+ group: "SIGNAL_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ sqliteDatabase: {
+ providers: [
+ providerBinding(SQLITE_PROVIDER_IDENTITY, {
+ group: "SQLITE_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ wasmArtifact: {
+ providers: [
+ providerBinding(WABT_PROVIDER_IDENTITY, ["inspect_wasm_artifact"]),
+ ],
+ },
+ webModuleTrace: {
+ providers: [
+ providerBinding(WEB_MODULE_TRACE_PROVIDER, ["trace_web_module_imports"]),
+ ],
+ },
+ webNetworkCapture: {
+ providers: [
+ providerBinding(SAZ_CAPTURE_PROVIDER_IDENTITY, {
+ group: "WEB_NETWORK_CAPTURE_TOOL_CONTRACTS",
+ }),
+ providerBinding(HAR_CAPTURE_PROVIDER_IDENTITY, {
+ group: "WEB_NETWORK_CAPTURE_TOOL_CONTRACTS",
+ }),
+ providerBinding(MITMPROXY_CAPTURE_PROVIDER_IDENTITY, {
+ group: "WEB_NETWORK_CAPTURE_TOOL_CONTRACTS",
+ }),
+ providerBinding(QLOG_CAPTURE_PROVIDER_IDENTITY, {
+ group: "WEB_NETWORK_CAPTURE_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ webRuntime: {
+ providers: [
+ providerBinding(CDP_BROWSER_PROVIDER_IDENTITY, {
+ group: "WEB_RUNTIME_TOOL_CONTRACTS",
+ }),
+ ],
+ },
+ webScriptExport: {
+ providers: [
+ providerBinding(WEB_SCRIPT_EXPORT_PROVIDER, ["export_web_scripts"]),
+ ],
+ },
+ webSourceLocation: {
+ providers: [
+ providerBinding(WEB_SOURCE_LOCATION_PROVIDER, [
+ "trace_web_source_location",
+ ]),
+ ],
+ },
+} as const satisfies Readonly>;
+
+export type CapabilityDeclarationId = keyof typeof CAPABILITY_DECLARATIONS;
diff --git a/src/composition/capabilityServices.ts b/src/composition/capabilityServices.ts
new file mode 100644
index 0000000000..67d28f2ace
--- /dev/null
+++ b/src/composition/capabilityServices.ts
@@ -0,0 +1,645 @@
+import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js";
+import {
+ locateWiresharkTool,
+ selectWiresharkTool,
+} from "../pcap/PcapConfiguration.js";
+import type { AnalysisSettingsReader } from "../domain/analysisSettings.js";
+import { CapabilityScope } from "../application/CapabilityScope.js";
+import type { CapabilityAvailabilityFacts } from "../application/CapabilityAvailability.js";
+import {
+ CAPABILITY_DECLARATIONS,
+ type CapabilityDeclarationId,
+} from "./capabilityDeclarations.js";
+import { isAbsolute } from "node:path";
+import { createGpuCodeObjectsService } from "./gpu.js";
+import { createPacketCaptureService, tsharkHostSupported } from "./captures.js";
+import {
+ createSignalRecordingService,
+ sigrokHostSupported,
+} from "./signals.js";
+import { wasmArtifactModule } from "./wasm.js";
+import { apkSignatureModule } from "./apkSignature.js";
+import {
+ evmInterfaceModule,
+ evmHostSupported,
+ evmLimiterAvailability,
+} from "./evm.js";
+import { createSqliteDatabaseService } from "./sqlite.js";
+import { createDesktopApplicationService } from "./desktop.js";
+import { createGoBinaryService } from "./go.js";
+import { createRustBinaryService } from "./rust.js";
+import { createBeamService } from "./erlang.js";
+import { createModelArtifactService } from "./model.js";
+import { createIl2cppMetadataService } from "./il2cpp.js";
+import { createElectronFuseService } from "./electronFuses.js";
+import { createPythonPycService } from "./pythonPyc.js";
+import { createWebNetworkCaptureService } from "./webNetworkCaptures.js";
+import { createWebSourceLocationService } from "./webSourceLocations.js";
+import { createWebModuleTraceService } from "./webModules.js";
+import { createWebRuntimeService } from "./webRuntime.js";
+import {
+ createJavaScriptRecoveryProvider,
+ wakaruHostSupported,
+} from "./javascriptRecovery.js";
+import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
+import { createReverseEngineeringService } from "./reverseEngineering.js";
+import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js";
+import {
+ createFirmwareAnalysisProvider,
+ firmwareHostSupported,
+} from "./firmware.js";
+import { createAndroidAnalysisProvider } from "./android.js";
+import { createPcapAnalysisProvider } from "./pcap.js";
+import { createFlutterBuildAnalysisProvider } from "./flutter.js";
+import { createApktoolResourceAnalysisProvider } from "./apktool.js";
+import { createJebAnalysisProvider } from "./jeb.js";
+import { createAdbDeviceAnalysisProvider } from "./adb.js";
+import { createFridaInstrumentationProvider } from "./frida.js";
+import { createCutterBridgeService } from "./cutter.js";
+import {
+ createGdbSessionManager,
+ createRizinDebugSessionManager,
+} from "./debuggerSessions.js";
+import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js";
+import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js";
+import {
+ createPeLoadConfigService,
+ createPeCertificatesService,
+ createPeImportsService,
+ createNativeExportsService,
+ createPeResourcesService,
+ createRecordedCrashService,
+ createBinaryLayoutService,
+ pwntoolsHostSupported,
+ createDevicetreeService,
+ createElfNotesService,
+ createAppleIpsService,
+ createKernelModuleService,
+} from "./binaryDiagnostics.js";
+
+import type { PacketCaptureService } from "../application/captures/PacketCaptureService.js";
+import type { SignalRecordingService } from "../application/signals/SignalRecordingService.js";
+import type { WasmArtifactService } from "../application/wasm/WasmArtifactService.js";
+import type { ApkSignatureService } from "../application/apkSignature/ApkSignatureService.js";
+import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
+import type { SqliteDatabaseService } from "../application/sqlite/SqliteDatabaseService.js";
+import type { Il2cppMetadataService } from "../application/il2cpp/Il2cppMetadataService.js";
+import type { GoBinaryService } from "../application/go/GoBinaryService.js";
+import type { ElectronFuseService } from "../application/electron/ElectronFuseService.js";
+import type { RustBinaryService } from "../application/rust/RustBinaryService.js";
+import type { PythonPycService } from "../application/python/PythonPycService.js";
+import type { BeamService } from "../application/erlang/BeamService.js";
+import type { RecordedCrashService } from "../application/binaryDiagnostics/RecordedCrashService.js";
+import type { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js";
+import type { BrowserObservationPort } from "../application/BrowserObservationPort.js";
+import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
+import type { ElectronActiveObservationPort } from "../application/javascript/ElectronActiveObservationPort.js";
+import type { ElectronObservationPort } from "../application/javascript/ElectronObservationPort.js";
+import type { JavaScriptRuntimeObservationPort } from "../application/javascript/JavaScriptRuntimeObservationPort.js";
+import type { FridaInstrumentationPort } from "../application/frida/FridaInstrumentationPort.js";
+import type { WebModuleTraceService } from "../application/WebModuleTraceService.js";
+import type { WebSourceLocationService } from "../application/WebSourceLocationService.js";
+import type { WebRuntimeService } from "../application/WebRuntimeService.js";
+import type { WebNetworkCaptureService } from "../application/WebNetworkCaptureService.js";
+import type { JavaScriptRecoveryPort } from "../application/javascript/JavaScriptRecoveryPort.js";
+import type { FirmwareAnalysisPort } from "../application/firmware/FirmwareAnalysisPort.js";
+import type { AndroidAnalysisPort } from "../application/android/AndroidAnalysisPort.js";
+import type { ApktoolResourceAnalysisPort } from "../application/apktool/ApktoolResourceAnalysisPort.js";
+import type { PcapAnalysisPort } from "../application/pcap/PcapAnalysisPort.js";
+import type { FlutterBuildAnalysisPort } from "../application/flutter/FlutterBuildAnalysisPort.js";
+import type { JebAnalysisPort } from "../application/jeb/JebAnalysisPort.js";
+import type { AdbDeviceAnalysisPort } from "../application/adb/AdbDeviceAnalysisPort.js";
+
+/** Typed injection inputs shared by a capability host, independent of transport. */
+export interface CapabilityServiceOptions {
+ readonly packetCapture?: PacketCaptureService;
+ readonly signalRecording?: SignalRecordingService;
+ readonly wasmArtifact?: WasmArtifactService;
+ readonly apkSignature?: ApkSignatureService;
+ readonly evmInterface?: EvmInterfaceService;
+ readonly sqliteDatabase?: SqliteDatabaseService;
+ readonly binaryLayout?: BinaryLayoutService;
+ readonly goBinary?: GoBinaryService;
+ readonly il2cppMetadata?: Il2cppMetadataService;
+ readonly rustBinary?: RustBinaryService;
+ readonly pythonPyc?: PythonPycService;
+ readonly erlangBeam?: BeamService;
+ readonly electronFuses?: ElectronFuseService;
+ readonly recordedCrash?: RecordedCrashService;
+ readonly firmwareAnalysis?: FirmwareAnalysisPort;
+ readonly javascriptRecovery?: JavaScriptRecoveryPort;
+ readonly webModuleTrace?: WebModuleTraceService;
+ readonly webSourceLocation?: WebSourceLocationService;
+ readonly webRuntime?: WebRuntimeService;
+ readonly webNetworkCapture?: WebNetworkCaptureService;
+ readonly androidAnalysis?: AndroidAnalysisPort;
+ readonly apktoolAnalysis?: ApktoolResourceAnalysisPort;
+ readonly pcapAnalysis?: PcapAnalysisPort;
+ readonly flutterAnalysis?: FlutterBuildAnalysisPort;
+ readonly jebAnalysis?: JebAnalysisPort;
+ readonly adbDeviceAnalysis?: AdbDeviceAnalysisPort;
+ readonly browserObservation?: BrowserObservationPort;
+ readonly browserScenarioCapture?: BrowserScenarioCapturePort;
+ readonly electronObservation?: ElectronObservationPort;
+ readonly optionalObservationProviderOwner?: "server" | "caller";
+ readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
+ readonly electronActiveObservation?: ElectronActiveObservationPort;
+ readonly javascriptRuntimeObservation?: JavaScriptRuntimeObservationPort;
+ readonly fridaInstrumentation?: FridaInstrumentationPort;
+}
+
+const tsharkLocatable = (environment: Readonly): boolean => {
+ try {
+ return (
+ locateWiresharkTool(
+ selectWiresharkTool(environment, "tshark"),
+ environment,
+ ) !== undefined
+ );
+ } catch {
+ // A relative REA_TSHARK_COMMAND is configuration the tool reports at execution.
+ return false;
+ }
+};
+
+export interface CapabilityHostContext {
+ readonly environment: Readonly;
+ readonly providerEnvironment: Readonly;
+ readonly settings: AnalysisSettingsReader;
+ readonly options: CapabilityServiceOptions;
+ readonly platform: NodeJS.Platform;
+ readonly arch: string;
+}
+
+interface CapabilityInstance {
+ readonly value: Value;
+ readonly close: () => Promise;
+ readonly ownership: "host" | "caller";
+ readonly availability?: CapabilityAvailabilityFacts;
+}
+
+const capability =
+ (
+ create: (context: CapabilityHostContext) => Value,
+ close: (value: Value) => Promise,
+ observe?: (
+ context: CapabilityHostContext,
+ value: Value,
+ ) => CapabilityAvailabilityFacts,
+ ownership: (context: CapabilityHostContext) => "host" | "caller" = () =>
+ "host",
+ ) =>
+ (context: CapabilityHostContext): CapabilityInstance => {
+ const value = create(context);
+ return {
+ value,
+ close: () => close(value),
+ ownership: ownership(context),
+ ...(observe === undefined
+ ? {}
+ : { availability: observe(context, value) }),
+ };
+ };
+
+const service = }>(
+ create: (context: CapabilityHostContext) => Value,
+ observe?: (
+ context: CapabilityHostContext,
+ value: Value,
+ ) => CapabilityAvailabilityFacts,
+) => capability(create, (value) => value.close(), observe);
+
+const outsideHost = (injected: unknown, supported: boolean): boolean =>
+ injected === undefined && !supported;
+const configuredPwntools = (context: CapabilityHostContext): boolean =>
+ pwntoolsHostSupported(context.platform, context.arch) &&
+ isAbsolute(context.environment.REA_PWNTOOLS_PYTHON ?? "");
+const sharedObservationOwnership = (context: CapabilityHostContext) =>
+ context.options.optionalObservationProviderOwner === "caller"
+ ? "caller"
+ : "host";
+const closeObservation = (value: { close?: () => Promise } | undefined) =>
+ value?.close?.() ?? Promise.resolve();
+
+/** Each entry binds construction, ownership and observed availability once. */
+const modules = {
+ wasmArtifact: service(
+ ({ environment, options, settings }) =>
+ wasmArtifactModule.create(environment, options.wasmArtifact, settings),
+ ({ environment, options }) => ({
+ optionalFeatures: {
+ wasmArtifactEnabled:
+ options.wasmArtifact !== undefined ||
+ isAbsolute(environment.REA_WABT_BIN_DIRECTORY ?? ""),
+ },
+ }),
+ ),
+ apkSignature: service(({ environment, options }) =>
+ apkSignatureModule.create(environment, options.apkSignature),
+ ),
+ evmInterface: service(
+ ({ environment, options }) =>
+ options.evmInterface ?? evmInterfaceModule.create(environment),
+ ({ environment, options, platform, arch }) => ({
+ hostUnsupported: {
+ evmInterface: outsideHost(
+ options.evmInterface,
+ evmHostSupported(platform, arch),
+ ),
+ },
+ optionalFeatures: {
+ evmInterfaceEnabled:
+ options.evmInterface !== undefined ||
+ evmHostSupported(platform, arch),
+ },
+ ...(options.evmInterface !== undefined ||
+ !evmHostSupported(platform, arch)
+ ? {}
+ : {
+ probes: {
+ evmLimiterAvailability: (signal: AbortSignal) =>
+ evmLimiterAvailability(environment, { signal }),
+ },
+ }),
+ }),
+ ),
+ sqliteDatabase: service(
+ ({ environment, options }) =>
+ options.sqliteDatabase ?? createSqliteDatabaseService(environment),
+ ),
+ binaryLayout: service(
+ ({ environment, options }) =>
+ options.binaryLayout ?? createBinaryLayoutService(environment),
+ (context) => ({
+ hostUnsupported: {
+ binaryLayout: outsideHost(
+ context.options.binaryLayout,
+ pwntoolsHostSupported(context.platform, context.arch),
+ ),
+ },
+ optionalFeatures: {
+ binaryLayoutEnabled:
+ context.options.binaryLayout !== undefined ||
+ configuredPwntools(context),
+ },
+ }),
+ ),
+ recordedCrash: service(
+ ({ environment, options }) =>
+ options.recordedCrash ?? createRecordedCrashService(environment),
+ (context) => ({
+ hostUnsupported: {
+ recordedCrash: outsideHost(
+ context.options.recordedCrash,
+ pwntoolsHostSupported(context.platform, context.arch),
+ ),
+ },
+ optionalFeatures: {
+ recordedCrashEnabled:
+ context.options.recordedCrash !== undefined ||
+ configuredPwntools(context),
+ },
+ }),
+ ),
+ webSourceLocation: service(
+ ({ environment, options }) =>
+ options.webSourceLocation ?? createWebSourceLocationService(environment),
+ ),
+ webModuleTrace: service(
+ ({ environment, options }) =>
+ options.webModuleTrace ?? createWebModuleTraceService(environment),
+ ({ environment, options }) => ({
+ optionalFeatures: {
+ webModuleResolutionEnabled:
+ options.webModuleTrace !== undefined ||
+ isAbsolute(environment.REA_BROWSER_EXECUTABLE ?? ""),
+ },
+ }),
+ ),
+ desktopApplication: service(() => createDesktopApplicationService()),
+ goBinary: service(
+ ({ options }) => options.goBinary ?? createGoBinaryService(),
+ ),
+ rustBinary: service(
+ ({ options }) => options.rustBinary ?? createRustBinaryService(),
+ ),
+ erlangBeam: service(
+ ({ options }) => options.erlangBeam ?? createBeamService(),
+ ),
+ modelArtifact: service(() => createModelArtifactService()),
+ gpuCodeObjects: service(() => createGpuCodeObjectsService()),
+ il2cppMetadata: service(
+ ({ options }) => options.il2cppMetadata ?? createIl2cppMetadataService(),
+ ),
+ peResources: service(() => createPeResourcesService()),
+ nativeExports: service(() => createNativeExportsService()),
+ devicetree: service(() => createDevicetreeService()),
+ elfNotes: service(() => createElfNotesService()),
+ appleIps: service(() => createAppleIpsService()),
+ kernelModule: service(() => createKernelModuleService()),
+ peLoadConfig: service(() => createPeLoadConfigService()),
+ peCertificates: service(() => createPeCertificatesService()),
+ peImports: service(() => createPeImportsService()),
+ electronFuses: service(
+ ({ options }) => options.electronFuses ?? createElectronFuseService(),
+ ),
+ pythonPyc: service(
+ ({ options }) => options.pythonPyc ?? createPythonPycService(),
+ ),
+ webNetworkCapture: service(
+ ({ environment, options, settings }) =>
+ options.webNetworkCapture ??
+ createWebNetworkCaptureService(environment, settings),
+ ),
+ javascriptRecovery: capability(
+ ({ environment, options }) =>
+ options.javascriptRecovery ??
+ createJavaScriptRecoveryProvider(environment),
+ (value) => value.close?.() ?? Promise.resolve(),
+ ({ environment, options, platform, arch }) => ({
+ hostUnsupported: {
+ javascriptRecovery: outsideHost(
+ options.javascriptRecovery,
+ wakaruHostSupported(platform, arch),
+ ),
+ },
+ optionalFeatures: {
+ javascriptRecoveryEnabled:
+ options.javascriptRecovery !== undefined ||
+ (wakaruHostSupported(platform, arch) &&
+ isAbsolute(environment.REA_WAKARU_COMMAND ?? "")),
+ },
+ }),
+ ),
+ artifactResources: service(() => new ArtifactResourceScope()),
+ reverseEngineering: service(({ providerEnvironment }) =>
+ createReverseEngineeringService(providerEnvironment),
+ ),
+ firmwareAnalysis: service(
+ ({ environment, options }) =>
+ new FirmwareAnalysisService(
+ options.firmwareAnalysis ?? createFirmwareAnalysisProvider(environment),
+ ),
+ ({ environment, options, platform }) => ({
+ hostUnsupported: {
+ firmwareInspection: outsideHost(
+ options.firmwareAnalysis,
+ firmwareHostSupported(platform),
+ ),
+ firmwareExtraction: outsideHost(
+ options.firmwareAnalysis,
+ firmwareHostSupported(platform),
+ ),
+ },
+ optionalFeatures: {
+ firmwareInspectionEnabled:
+ options.firmwareAnalysis !== undefined ||
+ (firmwareHostSupported(platform) &&
+ isAbsolute(environment.REA_BINWALK_COMMAND ?? "")),
+ firmwareExtractionEnabled:
+ options.firmwareAnalysis !== undefined ||
+ (firmwareHostSupported(platform) &&
+ isAbsolute(environment.REA_UNBLOB_COMMAND ?? "")),
+ },
+ }),
+ ),
+ gdbSessions: capability(
+ ({ providerEnvironment }) => createGdbSessionManager(providerEnvironment),
+ (value) => value.closeAll(),
+ ({ platform }) => ({
+ hostUnsupported: {
+ persistentDebugger: !persistentDebuggerHostSupported(platform),
+ },
+ }),
+ ),
+ rizinDebugSessions: capability(
+ ({ providerEnvironment }) =>
+ createRizinDebugSessionManager(providerEnvironment),
+ (value) => value.closeAll(),
+ ),
+ android: service(
+ ({ environment, options, settings }) =>
+ options.androidAnalysis ??
+ createAndroidAnalysisProvider(environment, undefined, settings),
+ (_context, value) => ({
+ probes: {
+ androidAnalysisAvailability: (signal: AbortSignal) =>
+ value.inspectAvailability(signal),
+ },
+ }),
+ ),
+ pcap: service(
+ ({ environment, options }) =>
+ options.pcapAnalysis ?? createPcapAnalysisProvider(environment),
+ ({ options, platform }) => ({
+ hostUnsupported: {
+ pcapCapture: outsideHost(
+ options.pcapAnalysis,
+ pcapCaptureHostSupported(platform),
+ ),
+ },
+ }),
+ ),
+ flutter: service(
+ ({ options }) =>
+ options.flutterAnalysis ?? createFlutterBuildAnalysisProvider(),
+ ),
+ fridaInstrumentation: capability(
+ ({ options }) =>
+ options.fridaInstrumentation ?? createFridaInstrumentationProvider(),
+ (value) => value.closeAll(),
+ ({ options }, value) => ({
+ observeProviderLoadFailures: () => {
+ const failure =
+ options.optionalProviderLoadFailures?.fridaInstrumentation ??
+ value.availabilityFailure?.();
+ return failure === undefined ? {} : { fridaInstrumentation: failure };
+ },
+ }),
+ ),
+ packetCapture: service(
+ ({ environment, options }) =>
+ options.packetCapture ?? createPacketCaptureService(environment),
+ ({ environment, options, platform }) => ({
+ hostUnsupported: {
+ packetCapture: outsideHost(
+ options.packetCapture,
+ tsharkHostSupported(platform),
+ ),
+ },
+ optionalFeatures: {
+ packetCaptureEnabled:
+ options.packetCapture !== undefined ||
+ (tsharkHostSupported(platform) && tsharkLocatable(environment)),
+ },
+ }),
+ ),
+ signalRecording: service(
+ ({ environment, options }) =>
+ options.signalRecording ?? createSignalRecordingService(environment),
+ ({ environment, options, platform }) => ({
+ hostUnsupported: {
+ signalRecording: outsideHost(
+ options.signalRecording,
+ sigrokHostSupported(platform),
+ ),
+ },
+ optionalFeatures: {
+ signalRecordingEnabled:
+ options.signalRecording !== undefined ||
+ (sigrokHostSupported(platform) &&
+ isAbsolute(environment.REA_SIGROK_COMMAND ?? "")),
+ },
+ }),
+ ),
+ webRuntime: capability(
+ ({ options }) => options.webRuntime ?? createWebRuntimeService(),
+ (value) => value.provider.close?.() ?? Promise.resolve(),
+ ),
+ apktool: service(
+ ({ environment, options }) =>
+ options.apktoolAnalysis ??
+ createApktoolResourceAnalysisProvider(environment),
+ (_context, value) => ({
+ probes: {
+ apktoolAnalysisAvailability: (signal: AbortSignal) =>
+ value.inspectAvailability(signal),
+ },
+ }),
+ ),
+ jeb: service(
+ ({ environment, options }) =>
+ options.jebAnalysis ?? createJebAnalysisProvider(environment),
+ (_context, value) => ({
+ probes: {
+ jebAnalysisAvailability: (signal: AbortSignal) =>
+ value.inspectAvailability(signal),
+ },
+ }),
+ ),
+ adbDevice: service(
+ ({ environment, options }) =>
+ options.adbDeviceAnalysis ?? createAdbDeviceAnalysisProvider(environment),
+ (_context, value) => ({
+ probes: {
+ adbAnalysisAvailability: (signal: AbortSignal) =>
+ value.inspectAvailability(signal),
+ },
+ }),
+ ),
+ cutter: service(({ providerEnvironment }) =>
+ createCutterBridgeService(providerEnvironment),
+ ),
+ browserObservation: capability(
+ ({ options }) => options.browserObservation,
+ closeObservation,
+ ({ options }) => ({
+ optionalFeatures: {
+ browserObservationEnabled: options.browserObservation !== undefined,
+ },
+ }),
+ sharedObservationOwnership,
+ ),
+ browserScenarioCapture: capability(
+ ({ options }) => options.browserScenarioCapture,
+ closeObservation,
+ ({ options }) => ({
+ optionalFeatures: {
+ browserScenarioEnabled: options.browserScenarioCapture !== undefined,
+ },
+ }),
+ sharedObservationOwnership,
+ ),
+ electronObservation: capability(
+ ({ options }) => options.electronObservation,
+ closeObservation,
+ ({ options }) => ({
+ optionalFeatures: {
+ electronObservationEnabled: options.electronObservation !== undefined,
+ },
+ }),
+ sharedObservationOwnership,
+ ),
+ electronActiveObservation: capability(
+ ({ options }) => options.electronActiveObservation,
+ closeObservation,
+ ({ options }) => ({
+ optionalFeatures: {
+ electronAutomationEnabled:
+ options.electronActiveObservation !== undefined,
+ },
+ }),
+ sharedObservationOwnership,
+ ),
+ javascriptRuntimeObservation: capability(
+ ({ options }) => options.javascriptRuntimeObservation,
+ closeObservation,
+ ({ options }) => ({
+ optionalFeatures: {
+ v8InspectorObservationEnabled:
+ options.javascriptRuntimeObservation !== undefined,
+ },
+ }),
+ sharedObservationOwnership,
+ ),
+} satisfies Partial<
+ Record<
+ CapabilityDeclarationId,
+ (context: CapabilityHostContext) => CapabilityInstance
+ >
+>;
+
+export type CapabilityServices = {
+ readonly [Key in keyof typeof modules]: ReturnType<
+ (typeof modules)[Key]
+ >["value"];
+};
+
+/** Compose every service and its cleanup together, retaining precise typed ports. */
+export const createCapabilityServices = (context: CapabilityHostContext) => {
+ const scope = new CapabilityScope();
+ const hostUnsupported: NonNullable<
+ CapabilityAvailabilityFacts["hostUnsupported"]
+ > = {};
+ const optionalFeatures: NonNullable<
+ CapabilityAvailabilityFacts["optionalFeatures"]
+ > = {};
+ const probes: NonNullable = {};
+ const failureObservers: (() => OptionalProviderLoadFailures)[] = [];
+ const availability = { hostUnsupported, optionalFeatures };
+ // This is a key-preserving map of a source-owned typed declaration, not an
+ // external value parser or a generic operation dispatcher.
+ const services = Object.fromEntries(
+ Object.entries(modules).map(([id, create]) => {
+ const instance = create(context);
+ const declaration = CAPABILITY_DECLARATIONS[id as keyof typeof modules];
+ if (declaration === undefined)
+ throw new Error(`Undeclared capability ${id}`);
+ Object.assign(
+ availability.hostUnsupported,
+ instance.availability?.hostUnsupported,
+ );
+ Object.assign(
+ availability.optionalFeatures,
+ instance.availability?.optionalFeatures,
+ );
+ Object.assign(probes, instance.availability?.probes);
+ const observeFailure = instance.availability?.observeProviderLoadFailures;
+ if (observeFailure !== undefined) failureObservers.push(observeFailure);
+ return [
+ id,
+ instance.ownership === "host"
+ ? scope.own(id, instance.value, instance.close)
+ : instance.value,
+ ];
+ }),
+ ) as CapabilityServices;
+ return {
+ services,
+ availability,
+ probes,
+ observeProviderLoadFailures: (): OptionalProviderLoadFailures =>
+ Object.assign({}, ...failureObservers.map((observe) => observe())),
+ close: () => scope.close(),
+ };
+};
diff --git a/src/composition/cutter.ts b/src/composition/cutter.ts
new file mode 100644
index 0000000000..aa2df990cf
--- /dev/null
+++ b/src/composition/cutter.ts
@@ -0,0 +1,11 @@
+import { CutterBridgeClient } from "../cutter/CutterBridgeClient.js";
+import { CutterBridgeService } from "../cutter/CutterBridgeService.js";
+
+/** Compose a fresh bridge owner without discovering or contacting Cutter. */
+export const createCutterBridgeService = (
+ environment: Readonly = process.env,
+ options: NonNullable<
+ ConstructorParameters[1]
+ > = {},
+): CutterBridgeService =>
+ new CutterBridgeService(new CutterBridgeClient(environment, options));
diff --git a/src/composition/debuggerSessions.ts b/src/composition/debuggerSessions.ts
new file mode 100644
index 0000000000..1a31db8ef9
--- /dev/null
+++ b/src/composition/debuggerSessions.ts
@@ -0,0 +1,20 @@
+import { GdbSessionManager } from "../gdb/GdbSessionManager.js";
+import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js";
+
+/** Allocate a GDB session owner; acquisition remains explicit in start(). */
+export const createGdbSessionManager = (
+ environment: Readonly = process.env,
+ options: Omit<
+ NonNullable[0]>,
+ "environment"
+ > = {},
+): GdbSessionManager => new GdbSessionManager({ ...options, environment });
+
+/** Allocate a Rizin debugger owner without launching a process or target. */
+export const createRizinDebugSessionManager = (
+ environment: Readonly = process.env,
+ options: NonNullable<
+ ConstructorParameters[1]
+ > = {},
+): RizinDebugSessionManager =>
+ new RizinDebugSessionManager(environment, options);
diff --git a/src/composition/frida.ts b/src/composition/frida.ts
new file mode 100644
index 0000000000..1de5ca82fa
--- /dev/null
+++ b/src/composition/frida.ts
@@ -0,0 +1,10 @@
+import type { FridaInstrumentationPort } from "../application/frida/FridaInstrumentationPort.js";
+import {
+ FridaInstrumentationManager,
+ type FridaInstrumentationManagerOptions,
+} from "../frida/FridaInstrumentationManager.js";
+
+/** Own a fresh Frida lifecycle; native bindings load only when requested. */
+export const createFridaInstrumentationProvider = (
+ options: FridaInstrumentationManagerOptions = {},
+): FridaInstrumentationPort => new FridaInstrumentationManager(options);
diff --git a/src/composition/reverseEngineering.ts b/src/composition/reverseEngineering.ts
index 493e86b9c2..e41561c8cb 100644
--- a/src/composition/reverseEngineering.ts
+++ b/src/composition/reverseEngineering.ts
@@ -1,14 +1,10 @@
import type { ReverseEngineeringServiceRuntimeOptions } from "../application/reverse/ReverseEngineeringService.js";
import type { ReverseEngineeringCommandPort } from "../application/reverse/ReverseEngineeringCommandPort.js";
import { ReverseEngineeringService } from "../application/reverse/ReverseEngineeringService.js";
-import {
- objdumpCommand,
- OBJDUMP_PROVIDER_IDENTITY,
-} from "../objdump/ObjdumpCommand.js";
-import {
- rizinCommand,
- RIZIN_PROVIDER_IDENTITY,
-} from "../rizin/RizinCommand.js";
+import { objdumpCommand } from "../objdump/ObjdumpCommand.js";
+import { OBJDUMP_PROVIDER_IDENTITY } from "../objdump/ObjdumpProviderIdentity.js";
+import { rizinCommand } from "../rizin/RizinCommand.js";
+import { RIZIN_PROVIDER_IDENTITY } from "../rizin/RizinProviderIdentity.js";
import { resolveWindowsProviderExecutable } from "./resolveWindowsProviderExecutable.js";
export const createReverseEngineeringCommandPort = (
diff --git a/src/contracts/toolFamilies.ts b/src/contracts/toolFamilies.ts
index 1980cfcad9..b89851d2a0 100644
--- a/src/contracts/toolFamilies.ts
+++ b/src/contracts/toolFamilies.ts
@@ -32,6 +32,8 @@ import { JEB_TOOL_CONTRACTS } from "./jeb/jebToolContracts.js";
import { ADB_TOOL_CONTRACTS } from "./adb/adbToolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js";
import { BROWSER_PROVIDER_TOOL_CONTRACTS } from "./browserProviderToolContracts.js";
+import { BROWSER_TOOL_CONTRACTS } from "./browserToolContracts.js";
+import { BROWSER_SCENARIO_TOOL_CONTRACTS } from "./browserScenarioToolContracts.js";
import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js";
import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "./javascript/javascriptRuntimeObservationToolContracts.js";
import { APPLICATION_TOOL_CONTRACTS } from "./applicationToolContracts.js";
@@ -46,6 +48,59 @@ import { FRIDA_TOOL_CONTRACTS } from "./fridaToolContracts.js";
import { TOOL_CONTRACTS } from "./toolContracts.js";
import type { ToolFamilyId } from "./toolFamilyIds.js";
+/** Canonical contract-array owners, independent of broader advertising families. */
+export const TOOL_CONTRACT_GROUPS = {
+ ADB_TOOL_CONTRACTS,
+ ANALYSIS_SETTINGS_TOOL_CONTRACTS,
+ ANALYSIS_VIEW_TOOL_CONTRACTS,
+ ANDROID_TOOL_CONTRACTS,
+ APKTOOL_TOOL_CONTRACTS,
+ APK_SIGNATURE_TOOL_CONTRACTS,
+ APPLICATION_TOOL_CONTRACTS,
+ ARTIFACT_TOOL_CONTRACTS,
+ BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
+ BROWSER_PROVIDER_TOOL_CONTRACTS,
+ BROWSER_SCENARIO_TOOL_CONTRACTS,
+ BROWSER_TOOL_CONTRACTS,
+ CAPTURE_TOOL_CONTRACTS,
+ CUTTER_TOOL_CONTRACTS,
+ DESKTOP_APPLICATION_TOOL_CONTRACTS,
+ ELECTRON_TOOL_CONTRACTS,
+ ENHANCED_TOOL_CONTRACTS,
+ ENVIRONMENT_TOOL_CONTRACTS,
+ ERLANG_BEAM_TOOL_CONTRACTS,
+ EVM_TOOL_CONTRACTS,
+ FIRMWARE_TOOL_CONTRACTS,
+ FLUTTER_TOOL_CONTRACTS,
+ FRIDA_TOOL_CONTRACTS,
+ GDB_TOOL_CONTRACTS,
+ GO_TOOL_CONTRACTS,
+ GPU_TOOL_CONTRACTS,
+ IL2CPP_TOOL_CONTRACTS,
+ JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
+ JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS,
+ JEB_TOOL_CONTRACTS,
+ MANAGED_TOOL_CONTRACTS,
+ MANAGED_WORKFLOW_TOOL_CONTRACTS,
+ MODEL_TOOL_CONTRACTS,
+ NATIVE_TOOL_CONTRACTS,
+ OFFICIAL_TOOL_CONTRACTS,
+ PCAP_TOOL_CONTRACTS,
+ PYTHON_PYC_TOOL_CONTRACTS,
+ REVERSE_ENGINEERING_TOOL_CONTRACTS,
+ RIZIN_DEBUG_TOOL_CONTRACTS,
+ RUST_TOOL_CONTRACTS,
+ SESSION_TOOL_CONTRACTS,
+ SIGNAL_TOOL_CONTRACTS,
+ SQLITE_TOOL_CONTRACTS,
+ WASM_TOOL_CONTRACTS,
+ WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
+ WEB_RUNTIME_TOOL_CONTRACTS,
+ WEB_SCRIPT_TOOL_CONTRACTS,
+} as const;
+
+export type ToolContractGroupId = keyof typeof TOOL_CONTRACT_GROUPS;
+
/** One selectable family: a stable selection id and its complete contract set. */
export interface ToolFamilyDefinition {
readonly id: ToolFamilyId;
@@ -62,107 +117,131 @@ export const TOOL_FAMILY_DEFINITIONS = [
{
id: "direct",
surface: "official-proxy",
- contracts: OFFICIAL_TOOL_CONTRACTS,
+ contracts: TOOL_CONTRACT_GROUPS.OFFICIAL_TOOL_CONTRACTS,
+ },
+ {
+ id: "enhanced",
+ surface: "enhanced",
+ contracts: TOOL_CONTRACT_GROUPS.ENHANCED_TOOL_CONTRACTS,
},
- { id: "enhanced", surface: "enhanced", contracts: ENHANCED_TOOL_CONTRACTS },
{
id: "native",
surface: "native-provider",
contracts: [
- ...NATIVE_TOOL_CONTRACTS,
- ...BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
- ...CUTTER_TOOL_CONTRACTS,
- ...REVERSE_ENGINEERING_TOOL_CONTRACTS,
- ...GDB_TOOL_CONTRACTS,
- ...RIZIN_DEBUG_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.NATIVE_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.CUTTER_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.REVERSE_ENGINEERING_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.GDB_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.RIZIN_DEBUG_TOOL_CONTRACTS,
],
},
{
id: "artifact",
surface: "artifact-provider",
contracts: [
- ...ARTIFACT_TOOL_CONTRACTS,
- ...DESKTOP_APPLICATION_TOOL_CONTRACTS,
- ...EVM_TOOL_CONTRACTS,
- ...GO_TOOL_CONTRACTS,
- ...GPU_TOOL_CONTRACTS,
- ...IL2CPP_TOOL_CONTRACTS,
- ...MODEL_TOOL_CONTRACTS,
- ...RUST_TOOL_CONTRACTS,
- ...PYTHON_PYC_TOOL_CONTRACTS,
- ...ERLANG_BEAM_TOOL_CONTRACTS,
- ...WASM_TOOL_CONTRACTS,
- ...APK_SIGNATURE_TOOL_CONTRACTS,
- ...SQLITE_TOOL_CONTRACTS,
- ...CAPTURE_TOOL_CONTRACTS,
- ...SIGNAL_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.ARTIFACT_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.DESKTOP_APPLICATION_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.EVM_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.GO_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.GPU_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.IL2CPP_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.MODEL_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.RUST_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.PYTHON_PYC_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.ERLANG_BEAM_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.WASM_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.APK_SIGNATURE_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.SQLITE_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.CAPTURE_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.SIGNAL_TOOL_CONTRACTS,
],
},
{
id: "managed",
surface: "managed",
- contracts: [...MANAGED_TOOL_CONTRACTS, ...MANAGED_WORKFLOW_TOOL_CONTRACTS],
+ contracts: [
+ ...TOOL_CONTRACT_GROUPS.MANAGED_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.MANAGED_WORKFLOW_TOOL_CONTRACTS,
+ ],
},
{
id: "firmware",
surface: "firmware-provider",
- contracts: FIRMWARE_TOOL_CONTRACTS,
+ contracts: TOOL_CONTRACT_GROUPS.FIRMWARE_TOOL_CONTRACTS,
},
{
id: "android",
surface: "android-provider",
- contracts: ANDROID_TOOL_CONTRACTS,
+ contracts: TOOL_CONTRACT_GROUPS.ANDROID_TOOL_CONTRACTS,
+ },
+ {
+ id: "pcap",
+ surface: "pcap-provider",
+ contracts: TOOL_CONTRACT_GROUPS.PCAP_TOOL_CONTRACTS,
},
- { id: "pcap", surface: "pcap-provider", contracts: PCAP_TOOL_CONTRACTS },
{
id: "flutter",
surface: "flutter-provider",
- contracts: FLUTTER_TOOL_CONTRACTS,
+ contracts: TOOL_CONTRACT_GROUPS.FLUTTER_TOOL_CONTRACTS,
},
{
id: "frida",
surface: "instrumentation-provider",
- contracts: FRIDA_TOOL_CONTRACTS,
+ contracts: TOOL_CONTRACT_GROUPS.FRIDA_TOOL_CONTRACTS,
},
{
id: "apktool",
surface: "apktool-provider",
- contracts: APKTOOL_TOOL_CONTRACTS,
+ contracts: TOOL_CONTRACT_GROUPS.APKTOOL_TOOL_CONTRACTS,
+ },
+ {
+ id: "jeb",
+ surface: "jeb-provider",
+ contracts: TOOL_CONTRACT_GROUPS.JEB_TOOL_CONTRACTS,
+ },
+ {
+ id: "adb",
+ surface: "adb-provider",
+ contracts: TOOL_CONTRACT_GROUPS.ADB_TOOL_CONTRACTS,
},
- { id: "jeb", surface: "jeb-provider", contracts: JEB_TOOL_CONTRACTS },
- { id: "adb", surface: "adb-provider", contracts: ADB_TOOL_CONTRACTS },
{
id: "browser",
surface: "browser-provider",
contracts: [
- ...BROWSER_PROVIDER_TOOL_CONTRACTS,
- ...WEB_RUNTIME_TOOL_CONTRACTS,
- ...WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.BROWSER_PROVIDER_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.WEB_RUNTIME_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
],
},
{
id: "electron",
surface: "electron-provider",
- contracts: ELECTRON_TOOL_CONTRACTS,
+ contracts: TOOL_CONTRACT_GROUPS.ELECTRON_TOOL_CONTRACTS,
},
{
id: "javascript-runtime",
surface: "runtime-provider",
- contracts: JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS,
+ contracts:
+ TOOL_CONTRACT_GROUPS.JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS,
},
{
id: "application",
surface: "application-workflow",
contracts: [
- ...APPLICATION_TOOL_CONTRACTS,
- ...ANALYSIS_SETTINGS_TOOL_CONTRACTS,
- ...ENVIRONMENT_TOOL_CONTRACTS,
- ...ANALYSIS_VIEW_TOOL_CONTRACTS,
- ...WEB_SCRIPT_TOOL_CONTRACTS,
- ...JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.APPLICATION_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.ANALYSIS_SETTINGS_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.ENVIRONMENT_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.ANALYSIS_VIEW_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.WEB_SCRIPT_TOOL_CONTRACTS,
+ ...TOOL_CONTRACT_GROUPS.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
],
},
- { id: "session", surface: "session", contracts: SESSION_TOOL_CONTRACTS },
+ {
+ id: "session",
+ surface: "session",
+ contracts: TOOL_CONTRACT_GROUPS.SESSION_TOOL_CONTRACTS,
+ },
] as const satisfies readonly ToolFamilyDefinition[];
const familyByTool = new Map();
diff --git a/src/cutter/CutterBridgeClient.ts b/src/cutter/CutterBridgeClient.ts
index 1bcbe9e43b..92f7497407 100644
--- a/src/cutter/CutterBridgeClient.ts
+++ b/src/cutter/CutterBridgeClient.ts
@@ -61,12 +61,6 @@ const descriptorSchema = z.object({
max_request_bytes: z.int().positive().optional(),
});
-export const CUTTER_PROVIDER_IDENTITY = {
- id: "cutter.python-plugin",
- name: "Cutter Python plugin",
- version: null,
-} as const;
-
const responseSchema = z.object({
ok: z.boolean(),
session_id: z.string().uuid().optional(),
diff --git a/src/cutter/CutterBridgeService.ts b/src/cutter/CutterBridgeService.ts
index 5fac87d6b4..987d4b4b16 100644
--- a/src/cutter/CutterBridgeService.ts
+++ b/src/cutter/CutterBridgeService.ts
@@ -16,8 +16,8 @@ import {
CutterBridgeClient,
type CutterBridgeExecution,
type CutterDiscoveryOptions,
- CUTTER_PROVIDER_IDENTITY,
} from "./CutterBridgeClient.js";
+import { CUTTER_PROVIDER_IDENTITY } from "./CutterProviderIdentity.js";
interface CutterBridgePort {
listSessions(
diff --git a/src/cutter/CutterProviderIdentity.ts b/src/cutter/CutterProviderIdentity.ts
new file mode 100644
index 0000000000..d38feb1767
--- /dev/null
+++ b/src/cutter/CutterProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const CUTTER_PROVIDER_IDENTITY = {
+ id: "cutter.python-plugin",
+ name: "Cutter Python plugin",
+ version: null,
+} as const;
diff --git a/src/domain/sqlite/SqliteProviderIdentity.ts b/src/domain/sqlite/SqliteProviderIdentity.ts
new file mode 100644
index 0000000000..d3acee6d01
--- /dev/null
+++ b/src/domain/sqlite/SqliteProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Format identity is shared by discovery, catalog, and observed SQLite results. */
+export const SQLITE_PROVIDER_IDENTITY = {
+ id: "sqlite",
+ name: "REA SQLite database inspector",
+ version: null,
+} as const;
diff --git a/src/domain/sqlite/sqliteDatabase.ts b/src/domain/sqlite/sqliteDatabase.ts
index 3f939dff20..ece4d30d62 100644
--- a/src/domain/sqlite/sqliteDatabase.ts
+++ b/src/domain/sqlite/sqliteDatabase.ts
@@ -2,13 +2,6 @@ import { z } from "zod";
import { absoluteLocalPathStringSchema } from "../localPath.js";
import type { JsonValue } from "../jsonValue.js";
-/** Format identity is shared by discovery, catalog, and observed SQLite results. */
-export const SQLITE_PROVIDER_IDENTITY = {
- id: "sqlite",
- name: "REA SQLite database inspector",
- version: null,
-} as const;
-
/** SQLite exposes this table as a row source but omits its own catalog entry. */
export const SQLITE_SCHEMA_TABLE_NAME = "sqlite_schema";
export const sqliteSchemaHasObjectDeclaration = (name: string): boolean =>
diff --git a/src/domain/webTraceProviders.ts b/src/domain/webTraceProviders.ts
new file mode 100644
index 0000000000..90373d0e34
--- /dev/null
+++ b/src/domain/webTraceProviders.ts
@@ -0,0 +1,11 @@
+/** Identities of the captured-source interpretation workflows. */
+export const WEB_SOURCE_LOCATION_PROVIDER = {
+ id: "rea-web-source-location",
+ name: "REA captured source-map point trace",
+ version: "1",
+} as const;
+export const WEB_MODULE_TRACE_PROVIDER = {
+ id: "rea-web-module-trace",
+ name: "REA captured module relationship trace",
+ version: "1",
+} as const;
diff --git a/src/electron/ElectronFuseFailures.ts b/src/electron/ElectronFuseFailures.ts
index 64714b3cc4..7a63d618d6 100644
--- a/src/electron/ElectronFuseFailures.ts
+++ b/src/electron/ElectronFuseFailures.ts
@@ -1,3 +1,4 @@
+import { ELECTRON_FUSE_PROVIDER_IDENTITY } from "./ElectronFuseProviderIdentity.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
ArtifactFileCleanupFailure,
@@ -31,13 +32,6 @@ import {
ElectronFuseResourceFailure,
} from "./ElectronFuseReader.js";
-/** Bundled fuse-wire reader. It does not flip fuses or launch the application. */
-export const ELECTRON_FUSE_PROVIDER_IDENTITY = {
- id: "electron-fuses",
- name: "REA Electron fuse reader",
- version: "1",
-} as const;
-
const OPERATION = "inspect_electron_fuses";
/** Translate the shared fuse reader without replacing its producing outcome or cleanup owner. */
diff --git a/src/electron/ElectronFuseProvider.ts b/src/electron/ElectronFuseProvider.ts
index a21d5cfbdc..ffae6fbc74 100644
--- a/src/electron/ElectronFuseProvider.ts
+++ b/src/electron/ElectronFuseProvider.ts
@@ -21,12 +21,8 @@ import {
readElectronFuseImage,
type ElectronFuseImage,
} from "./ElectronFuseReader.js";
-import {
- ELECTRON_FUSE_PROVIDER_IDENTITY,
- electronFuseFailure,
-} from "./ElectronFuseFailures.js";
-
-export { ELECTRON_FUSE_PROVIDER_IDENTITY };
+import { electronFuseFailure } from "./ElectronFuseFailures.js";
+import { ELECTRON_FUSE_PROVIDER_IDENTITY } from "./ElectronFuseProviderIdentity.js";
const OPERATION = "inspect_electron_fuses";
diff --git a/src/electron/ElectronFuseProviderIdentity.ts b/src/electron/ElectronFuseProviderIdentity.ts
new file mode 100644
index 0000000000..18b675fbd8
--- /dev/null
+++ b/src/electron/ElectronFuseProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Bundled fuse-wire reader. It does not flip fuses or launch the application. */
+export const ELECTRON_FUSE_PROVIDER_IDENTITY = {
+ id: "electron-fuses",
+ name: "REA Electron fuse reader",
+ version: "1",
+} as const;
diff --git a/src/erlang/BeamProvider.ts b/src/erlang/BeamProvider.ts
index 4aa2301115..bbee47ca82 100644
--- a/src/erlang/BeamProvider.ts
+++ b/src/erlang/BeamProvider.ts
@@ -1,3 +1,4 @@
+import { ERLANG_BEAM_PROVIDER_IDENTITY } from "./ErlangBeamProviderIdentity.js";
import {
projectStableArtifactFailure,
projectArtifactCloseFailure,
@@ -30,13 +31,6 @@ import {
import { err, ok } from "../domain/result.js";
import { BeamCapacityFailure, parseErlangBeam } from "./BeamParser.js";
-/** Bundled format reader identity; module names are separate observed facts. */
-export const ERLANG_BEAM_PROVIDER_IDENTITY = {
- id: "rea-erlang-beam",
- name: "REA Erlang BEAM reader",
- version: "1",
-} as const;
-
const OPERATION = "inspect_erlang_beam";
/** Portable parser over one bounded, identity-checked regular file. */
diff --git a/src/erlang/ErlangBeamProviderIdentity.ts b/src/erlang/ErlangBeamProviderIdentity.ts
new file mode 100644
index 0000000000..f26529202f
--- /dev/null
+++ b/src/erlang/ErlangBeamProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Bundled format reader identity; module names are separate observed facts. */
+export const ERLANG_BEAM_PROVIDER_IDENTITY = {
+ id: "rea-erlang-beam",
+ name: "REA Erlang BEAM reader",
+ version: "1",
+} as const;
diff --git a/src/evm/EvmoleFailures.ts b/src/evm/EvmoleFailures.ts
index 190e3b7f9e..3ae76c1b21 100644
--- a/src/evm/EvmoleFailures.ts
+++ b/src/evm/EvmoleFailures.ts
@@ -28,11 +28,11 @@ import {
MAX_EVM_TIMEOUT_MS,
} from "../domain/evm/evmInterface.js";
import {
- EVMOLE_PROVIDER_IDENTITY,
EVM_CPU_SECONDS,
EVM_INTERFACE_LIMITS,
EVM_FILE_SIZE_FAILURE_EXIT,
} from "./EvmoleRelease.js";
+import { EVMOLE_PROVIDER_IDENTITY } from "./EvmoleProviderIdentity.js";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
/** Both a structured worker refusal and reply-file admission name this allowance. */
diff --git a/src/evm/EvmoleInterfaceProvider.ts b/src/evm/EvmoleInterfaceProvider.ts
index 4f50055207..b8f41cf152 100644
--- a/src/evm/EvmoleInterfaceProvider.ts
+++ b/src/evm/EvmoleInterfaceProvider.ts
@@ -76,11 +76,11 @@ import {
projectEvmoleFunctions,
} from "./EvmoleInterfaceAdapter.js";
import {
- EVMOLE_PROVIDER_IDENTITY,
EVM_CPU_SECONDS,
EVM_INTERFACE_LIMITS,
EVM_FILE_SIZE_FAILURE_EXIT,
} from "./EvmoleRelease.js";
+import { EVMOLE_PROVIDER_IDENTITY } from "./EvmoleProviderIdentity.js";
import { decodeEvmBytecodeCarrier } from "./EvmBytecodeCarrier.js";
import {
selectEvmWorkerLimits,
diff --git a/src/evm/EvmoleProviderIdentity.ts b/src/evm/EvmoleProviderIdentity.ts
new file mode 100644
index 0000000000..492171551b
--- /dev/null
+++ b/src/evm/EvmoleProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Unchanged locked npm/WASM profile; provenance is independent of chain deployment authenticity. */
+export const EVMOLE_PROVIDER_IDENTITY = {
+ id: "evmole-interface",
+ name: "REA EVM interface adapter",
+ version: "evmole@0.9.3",
+} as const;
diff --git a/src/evm/EvmoleRelease.ts b/src/evm/EvmoleRelease.ts
index 7dd3a181e6..4432ce366a 100644
--- a/src/evm/EvmoleRelease.ts
+++ b/src/evm/EvmoleRelease.ts
@@ -1,11 +1,5 @@
import { DEFAULT_EVM_TIMEOUT_MS } from "../domain/evm/evmInterface.js";
-/** Unchanged locked npm/WASM profile; provenance is independent of chain deployment authenticity. */
-export const EVMOLE_PROVIDER_IDENTITY = {
- id: "evmole-interface",
- name: "REA EVM interface adapter",
- version: "evmole@0.9.3",
-} as const;
/** Worker-reserved status for an observed EFBIG write without a complete reply. */
export const EVM_FILE_SIZE_FAILURE_EXIT = 76;
/** Complete offline inspection budgets; worker JavaScript heap is bounded separately. */
diff --git a/src/firmware/FirmwareProvider.ts b/src/firmware/FirmwareProvider.ts
index 6fbc7cff96..377095073d 100644
--- a/src/firmware/FirmwareProvider.ts
+++ b/src/firmware/FirmwareProvider.ts
@@ -31,7 +31,7 @@ import { publishFirmwareExtraction } from "./FirmwarePublication.js";
import {
BINWALK_PROVIDER_IDENTITY,
UNBLOB_PROVIDER_IDENTITY,
-} from "./FirmwareRelease.js";
+} from "./FirmwareProviderIdentities.js";
import {
startFirmwareDeadline,
type FirmwareDeadline,
diff --git a/src/firmware/FirmwareProviderIdentities.ts b/src/firmware/FirmwareProviderIdentities.ts
new file mode 100644
index 0000000000..cfe77d685e
--- /dev/null
+++ b/src/firmware/FirmwareProviderIdentities.ts
@@ -0,0 +1,15 @@
+import { FIRMWARE_TOOL_REQUIREMENTS } from "../contracts/firmware/firmwareToolRequirements.js";
+
+/** Catalog identity for the verified Binwalk release. Evidence replaces version with the observed banner. */
+export const BINWALK_PROVIDER_IDENTITY = {
+ id: "binwalk",
+ name: FIRMWARE_TOOL_REQUIREMENTS.binwalk.name,
+ version: FIRMWARE_TOOL_REQUIREMENTS.binwalk.version,
+} as const;
+
+/** Catalog identity for explicit firmware extraction. */
+export const UNBLOB_PROVIDER_IDENTITY = {
+ id: "unblob",
+ name: FIRMWARE_TOOL_REQUIREMENTS.unblob.name,
+ version: FIRMWARE_TOOL_REQUIREMENTS.unblob.version,
+} as const;
diff --git a/src/firmware/FirmwareRelease.ts b/src/firmware/FirmwareRelease.ts
index 42c96f3c2f..653cdea3b9 100644
--- a/src/firmware/FirmwareRelease.ts
+++ b/src/firmware/FirmwareRelease.ts
@@ -1,18 +1,4 @@
import { BOUNDED_PRODUCER_LIMITS } from "../process/LinuxResourceLimits.js";
-import { FIRMWARE_TOOL_REQUIREMENTS } from "../contracts/firmware/firmwareToolRequirements.js";
-
-/** Catalog identity for the verified Binwalk release. Evidence replaces version with the observed banner. */
-export const BINWALK_PROVIDER_IDENTITY = {
- id: "binwalk",
- name: FIRMWARE_TOOL_REQUIREMENTS.binwalk.name,
- version: FIRMWARE_TOOL_REQUIREMENTS.binwalk.version,
-} as const;
-/** Catalog identity for explicit firmware extraction. */
-export const UNBLOB_PROVIDER_IDENTITY = {
- id: "unblob",
- name: FIRMWARE_TOOL_REQUIREMENTS.unblob.name,
- version: FIRMWARE_TOOL_REQUIREMENTS.unblob.version,
-} as const;
/** Resource policy for this Linux integration, not a claim of aggregate containment. */
export const FIRMWARE_LIMITS = {
diff --git a/src/flutter/FlutterBuildProvider.ts b/src/flutter/FlutterBuildProvider.ts
index 185979e052..8cf8cbc056 100644
--- a/src/flutter/FlutterBuildProvider.ts
+++ b/src/flutter/FlutterBuildProvider.ts
@@ -1,3 +1,4 @@
+import { FLUTTER_PROVIDER_IDENTITY } from "./FlutterProviderIdentity.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import {
AnalysisCleanupLease,
@@ -66,13 +67,6 @@ import {
import { readElfBuildIds } from "./ElfBuildId.js";
import { privateWorkspaceFailure } from "../process/privateWorkspaceFailure.js";
-/** Stable identity for the Flutter build-identification provider. */
-export const FLUTTER_PROVIDER_IDENTITY = {
- id: "flutter",
- name: "Flutter build identification",
- version: null,
-} as const;
-
const APK_METADATA_READ_MAX_BYTES = 8 * 1024 * 1024;
const LIBRARY_MAX_BYTES = 256 * 1024 * 1024;
/** Cumulative cap across every library entry in one identification. */
diff --git a/src/flutter/FlutterProviderIdentity.ts b/src/flutter/FlutterProviderIdentity.ts
new file mode 100644
index 0000000000..60749ce6da
--- /dev/null
+++ b/src/flutter/FlutterProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Stable identity for the Flutter build-identification provider. */
+export const FLUTTER_PROVIDER_IDENTITY = {
+ id: "flutter",
+ name: "Flutter build identification",
+ version: null,
+} as const;
diff --git a/src/application/frida/FridaInstrumentationManager.test.ts b/src/frida/FridaInstrumentationManager.test.ts
similarity index 98%
rename from src/application/frida/FridaInstrumentationManager.test.ts
rename to src/frida/FridaInstrumentationManager.test.ts
index af5c37a377..a0c4a0e1a0 100644
--- a/src/application/frida/FridaInstrumentationManager.test.ts
+++ b/src/frida/FridaInstrumentationManager.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it, onTestFinished } from "vitest";
-import { createDeferred } from "../../../tests/fixtures/binarySession.js";
+import { createDeferred } from "../../tests/fixtures/binarySession.js";
import type {
Device,
@@ -10,18 +10,18 @@ import type {
Session,
Signal,
} from "frida";
-import { jsonObjectSchema } from "../../domain/jsonValue.js";
-import { FridaScriptLoadCancelledError } from "./FridaInstrumentationPort.js";
+import { jsonObjectSchema } from "../domain/jsonValue.js";
+import { FridaScriptLoadCancelledError } from "../application/frida/FridaInstrumentationPort.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisTimeoutError,
-} from "../../domain/analysisErrorCore.js";
-import { FridaInstrumentationManager } from "../../frida/FridaInstrumentationManager.js";
-import { fridaCaptureLimitations } from "../../frida/fridaCaptureLimits.js";
-import { AnalysisError } from "../../domain/analysisErrorBase.js";
-import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
-import { FridaInstrumentationService } from "./FridaInstrumentationService.js";
+} from "../domain/analysisErrorCore.js";
+import { FridaInstrumentationManager } from "./FridaInstrumentationManager.js";
+import { fridaCaptureLimitations } from "./fridaCaptureLimits.js";
+import { AnalysisError } from "../domain/analysisErrorBase.js";
+import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
+import { FridaInstrumentationService } from "../application/frida/FridaInstrumentationService.js";
const signal = void>() => {
let connected: Handler | undefined;
diff --git a/src/frida/FridaInstrumentationManager.ts b/src/frida/FridaInstrumentationManager.ts
index 9e6fc01276..bbde915936 100644
--- a/src/frida/FridaInstrumentationManager.ts
+++ b/src/frida/FridaInstrumentationManager.ts
@@ -68,12 +68,6 @@ const isFridaDeviceLookupTimeout = (cause: unknown): boolean =>
cause instanceof Error &&
cause.message === FRIDA_DEVICE_LOOKUP_TIMEOUT_MESSAGE;
-export const FRIDA_PROVIDER_IDENTITY = {
- id: "frida",
- name: "Frida",
- version: null,
-} as const;
-
interface ScriptMessageCapture {
readonly messages: MessageFifo;
truncated: boolean;
diff --git a/src/frida/FridaProviderIdentity.ts b/src/frida/FridaProviderIdentity.ts
new file mode 100644
index 0000000000..620805bb2f
--- /dev/null
+++ b/src/frida/FridaProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const FRIDA_PROVIDER_IDENTITY = {
+ id: "frida",
+ name: "Frida",
+ version: null,
+} as const;
diff --git a/src/gdb/GdbProviderIdentity.ts b/src/gdb/GdbProviderIdentity.ts
new file mode 100644
index 0000000000..76baf799b2
--- /dev/null
+++ b/src/gdb/GdbProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const GDB_PROVIDER_IDENTITY = {
+ id: "gnu.gdb",
+ name: "GNU GDB",
+ version: null,
+} as const;
diff --git a/src/gdb/GdbSessionManager.ts b/src/gdb/GdbSessionManager.ts
index 350b866741..cf9047bc6d 100644
--- a/src/gdb/GdbSessionManager.ts
+++ b/src/gdb/GdbSessionManager.ts
@@ -1,3 +1,4 @@
+import { GDB_PROVIDER_IDENTITY } from "./GdbProviderIdentity.js";
import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js";
import { ABORTED, waitForAbortable } from "../process/AbortablePromise.js";
import { snapshotGdbCommandRecords } from "./GdbCommandRecords.js";
@@ -115,12 +116,6 @@ const cancelledWithCallerReason = (
return cancellation;
};
-export const GDB_PROVIDER_IDENTITY = {
- id: "gnu.gdb",
- name: "GNU GDB",
- version: null,
-} as const;
-
interface PendingCommand {
readonly token: number;
readonly command: string;
diff --git a/src/ghidra/GhidraFunctionOperations.ts b/src/ghidra/GhidraFunctionOperations.ts
new file mode 100644
index 0000000000..9cd1d9ca96
--- /dev/null
+++ b/src/ghidra/GhidraFunctionOperations.ts
@@ -0,0 +1,16 @@
+/** Function analysis and annotation operations admitted by the Ghidra adapter. */
+export const GHIDRA_FUNCTION_OPERATIONS = [
+ "annotate_native_function",
+ "inspect_native_data_type",
+ "inspect_native_instruction",
+ "resolve_native_call_targets",
+ "analyze_function",
+ "procedure_assembly",
+ "procedure_callees",
+ "procedure_callers",
+ "procedure_info",
+ "procedure_pseudo_code",
+ "read_function_instructions",
+ "procedure_references",
+ "xrefs",
+] as const;
diff --git a/src/ghidra/GhidraFunctionValues.ts b/src/ghidra/GhidraFunctionValues.ts
index dc24c22871..57c78761a0 100644
--- a/src/ghidra/GhidraFunctionValues.ts
+++ b/src/ghidra/GhidraFunctionValues.ts
@@ -1,3 +1,4 @@
+import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionOperations.js";
import { nativeAddressObservationSchema } from "../domain/native/nativeAddress.js";
import { procedureInfoSchema } from "../domain/native/procedureInfo.js";
import { z } from "zod";
@@ -55,23 +56,6 @@ import {
ghidraFunctionIdentitySchema as dossierProcedureIdentity,
} from "./GhidraInventoryValues.js";
-/** Function analysis and annotation operations admitted by the Ghidra adapter. */
-export const GHIDRA_FUNCTION_OPERATIONS = [
- "annotate_native_function",
- "inspect_native_data_type",
- "inspect_native_instruction",
- "resolve_native_call_targets",
- "analyze_function",
- "procedure_assembly",
- "procedure_callees",
- "procedure_callers",
- "procedure_info",
- "procedure_pseudo_code",
- "read_function_instructions",
- "procedure_references",
- "xrefs",
-] as const;
-
/** One function-analysis operation implemented by the packaged Java bridge. */
export type GhidraFunctionOperation =
(typeof GHIDRA_FUNCTION_OPERATIONS)[number];
diff --git a/src/ghidra/GhidraInventoryOperations.ts b/src/ghidra/GhidraInventoryOperations.ts
new file mode 100644
index 0000000000..7075da5c42
--- /dev/null
+++ b/src/ghidra/GhidraInventoryOperations.ts
@@ -0,0 +1,23 @@
+/**
+ * Direct inventory operations admitted by the Ghidra adapter. All are reads
+ * except the naming operations, which edit the ephemeral session database.
+ */
+export const GHIDRA_INVENTORY_OPERATIONS = [
+ "read_binary_inventory_summary",
+ "inspect_native_load_image",
+ "read_bytes",
+ "search_bytes",
+ "address_to_file_offset",
+ "address_name",
+ "list_documents",
+ "list_names",
+ "list_procedures",
+ "list_segments",
+ "list_strings",
+ "procedure_address",
+ "resolve_containing_procedure",
+ "search_procedures",
+ "search_strings",
+ "set_address_name",
+ "set_addresses_names",
+] as const;
diff --git a/src/ghidra/GhidraInventoryValues.ts b/src/ghidra/GhidraInventoryValues.ts
index 974050e777..56da3cdd2d 100644
--- a/src/ghidra/GhidraInventoryValues.ts
+++ b/src/ghidra/GhidraInventoryValues.ts
@@ -1,3 +1,4 @@
+import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryOperations.js";
import { nativeSearchFields } from "../domain/native/nativeSearch.js";
import { nativeSymbolMetadataSchema } from "../domain/native/nativeSymbol.js";
import { nativeAddressObservationSchema } from "../domain/native/nativeAddress.js";
@@ -30,30 +31,6 @@ import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
-/**
- * Direct inventory operations admitted by the Ghidra adapter. All are reads
- * except the naming operations, which edit the ephemeral session database.
- */
-export const GHIDRA_INVENTORY_OPERATIONS = [
- "read_binary_inventory_summary",
- "inspect_native_load_image",
- "read_bytes",
- "search_bytes",
- "address_to_file_offset",
- "address_name",
- "list_documents",
- "list_names",
- "list_procedures",
- "list_segments",
- "list_strings",
- "procedure_address",
- "resolve_containing_procedure",
- "search_procedures",
- "search_strings",
- "set_address_name",
- "set_addresses_names",
-] as const;
-
/** Inventory operations that write analyst names into the session database. */
export const GHIDRA_NAMING_OPERATIONS = [
"set_address_name",
diff --git a/src/ghidra/GhidraProvider.ts b/src/ghidra/GhidraProvider.ts
index 8f40e43880..860c835746 100644
--- a/src/ghidra/GhidraProvider.ts
+++ b/src/ghidra/GhidraProvider.ts
@@ -75,9 +75,9 @@ import {
existingProjectCapabilities,
EXISTING_PROJECT_WINDOWS_UNAVAILABLE,
windowsP0Capabilities,
- GHIDRA_PROVIDER_IDENTITY,
WINDOWS_P0_NATIVE_PE_ARCHITECTURES,
} from "./GhidraProviderCapabilities.js";
+import { GHIDRA_PROVIDER_IDENTITY } from "./GhidraProviderMetadata.js";
import {
createGhidraProviderClient,
admitGhidraClient,
diff --git a/src/ghidra/GhidraProviderCapabilities.ts b/src/ghidra/GhidraProviderCapabilities.ts
index 4fe7604fb9..0714b6f7cc 100644
--- a/src/ghidra/GhidraProviderCapabilities.ts
+++ b/src/ghidra/GhidraProviderCapabilities.ts
@@ -1,13 +1,11 @@
-import type {
- CapabilityDescriptor,
- ProviderIdentity,
-} from "../application/AnalysisProvider.js";
+import { GHIDRA_PROVIDER_IDENTITY } from "./GhidraProviderMetadata.js";
+import type { CapabilityDescriptor } from "../application/AnalysisProvider.js";
import {
inspectWindowsNativeReadiness,
type WindowsNativeReadiness,
} from "../process/WindowsAuthority.js";
-import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js";
-import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js";
+import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionOperations.js";
+import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryOperations.js";
import { GHIDRA_MUTATING_OPERATIONS } from "./GhidraSessionValues.js";
import { ghidraLoadImageVerificationNote } from "./GhidraVerifiedLoadImageFormats.js";
@@ -15,28 +13,12 @@ import { ghidraLoadImageVerificationNote } from "./GhidraVerifiedLoadImageFormat
export const WINDOWS_P0_NATIVE_PE_ARCHITECTURES: readonly string[] =
Object.freeze(["x86", "x86_64", "arm", "arm64"]);
-/** Public identity committed by every Ghidra-backed observation. */
-export const GHIDRA_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
- id: "ghidra",
- name: "Ghidra",
- version: null,
-});
-
/** Provider observations, including internal aggregates consumed by workflows. */
const providerOperations = Object.freeze([
...GHIDRA_INVENTORY_OPERATIONS,
...GHIDRA_FUNCTION_OPERATIONS,
]);
-/** Public analyst tools backed by these observations; aggregates stay internal. */
-export const GHIDRA_OPERATIONS = Object.freeze(
- providerOperations.map((operation) =>
- operation === "read_binary_inventory_summary"
- ? "binary_overview"
- : operation,
- ),
-);
-
/** Health limitations shared by every Ghidra-backed capability. */
export const healthLimitations = Object.freeze([
"The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.",
diff --git a/src/ghidra/GhidraProviderClient.ts b/src/ghidra/GhidraProviderClient.ts
index 90d069c743..dc8f3e5876 100644
--- a/src/ghidra/GhidraProviderClient.ts
+++ b/src/ghidra/GhidraProviderClient.ts
@@ -101,11 +101,11 @@ import { attestGhidraNativeLoadImage } from "./GhidraLoadImageAttest.js";
import type { NativeAotPeResult } from "../domain/native/nativeAotPe.js";
import { GhidraNativeAotSnapshotOwner } from "./GhidraNativeAotSnapshotOwner.js";
import {
- GHIDRA_PROVIDER_IDENTITY,
ghidraHealthLimitations,
windowsP0Limitations,
limitationsFor,
} from "./GhidraProviderCapabilities.js";
+import { GHIDRA_PROVIDER_IDENTITY } from "./GhidraProviderMetadata.js";
import {
GHIDRA_STARTUP_DEADLINE_ELAPSED,
isGhidraInputFailureCode,
diff --git a/src/ghidra/GhidraProviderMetadata.ts b/src/ghidra/GhidraProviderMetadata.ts
new file mode 100644
index 0000000000..f8bc9b8b73
--- /dev/null
+++ b/src/ghidra/GhidraProviderMetadata.ts
@@ -0,0 +1,20 @@
+import type { ProviderIdentity } from "../application/AnalysisProvider.js";
+import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionOperations.js";
+import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryOperations.js";
+
+/** Public identity committed by every Ghidra-backed observation. */
+export const GHIDRA_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
+ id: "ghidra",
+ name: "Ghidra",
+ version: null,
+});
+
+/** Public analyst tools backed by these observations; aggregates stay internal. */
+export const GHIDRA_OPERATIONS = Object.freeze(
+ [...GHIDRA_INVENTORY_OPERATIONS, ...GHIDRA_FUNCTION_OPERATIONS].map(
+ (operation) =>
+ operation === "read_binary_inventory_summary"
+ ? "binary_overview"
+ : operation,
+ ),
+);
diff --git a/src/ghidra/GhidraSessionValues.ts b/src/ghidra/GhidraSessionValues.ts
index 917be2e46e..f0b7c031ac 100644
--- a/src/ghidra/GhidraSessionValues.ts
+++ b/src/ghidra/GhidraSessionValues.ts
@@ -5,11 +5,9 @@ import { z } from "zod";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
-import {
- GHIDRA_INVENTORY_OPERATIONS,
- GHIDRA_NAMING_OPERATIONS,
-} from "./GhidraInventoryValues.js";
-import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js";
+import { GHIDRA_NAMING_OPERATIONS } from "./GhidraInventoryValues.js";
+import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryOperations.js";
+import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionOperations.js";
import { ghidraExtensionResultSchema } from "./extensions/GhidraExtensions.js";
import { GHIDRA_MIPS_SOFT_FLOAT_STORAGE } from "./GhidraMipsProfile.js";
import { GHIDRA_RISCV_CALLING_CONVENTION_IDS } from "./GhidraRiscvProfile.js";
diff --git a/src/go/GoBinaryProvider.ts b/src/go/GoBinaryProvider.ts
index 54b12fe726..5f59bf7f75 100644
--- a/src/go/GoBinaryProvider.ts
+++ b/src/go/GoBinaryProvider.ts
@@ -1,3 +1,4 @@
+import { GO_BINARY_PROVIDER_IDENTITY } from "./GoBinaryProviderIdentity.js";
import {
projectStableArtifactFailure,
projectArtifactCloseFailure,
@@ -39,12 +40,6 @@ import {
BinaryResourceFailure,
} from "../native/metadata/BinaryContainer.js";
-/** Bundled format reader identity; embedded compiler versions are separate observed facts. */
-export const GO_BINARY_PROVIDER_IDENTITY = {
- id: "go-build-info",
- name: "REA Go build-information reader",
- version: "1",
-} as const;
const OPERATION = "inspect_go_binary";
const INPUT_BYTES = 256 * 1024 * 1024;
const OUTPUT_BYTES = 8 * 1024 * 1024;
diff --git a/src/go/GoBinaryProviderIdentity.ts b/src/go/GoBinaryProviderIdentity.ts
new file mode 100644
index 0000000000..979e183235
--- /dev/null
+++ b/src/go/GoBinaryProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Bundled format reader identity; embedded compiler versions are separate observed facts. */
+export const GO_BINARY_PROVIDER_IDENTITY = {
+ id: "go-build-info",
+ name: "REA Go build-information reader",
+ version: "1",
+} as const;
diff --git a/src/gpu/GpuCodeObjectsProvider.ts b/src/gpu/GpuCodeObjectsProvider.ts
index 78c87e72d1..3b02ba7e63 100644
--- a/src/gpu/GpuCodeObjectsProvider.ts
+++ b/src/gpu/GpuCodeObjectsProvider.ts
@@ -1,3 +1,4 @@
+import { GPU_CODE_OBJECTS_PROVIDER_IDENTITY } from "./GpuCodeObjectsProviderIdentity.js";
import { lstat, type FileHandle } from "node:fs/promises";
import type { GpuCodeObjectsPort } from "../application/gpu/GpuCodeObjectsPort.js";
import {
@@ -51,11 +52,6 @@ import {
sameRegularFileState,
} from "../filesystem/RegularFile.js";
-export const GPU_CODE_OBJECTS_PROVIDER_IDENTITY = {
- id: "gpu-code-objects",
- name: "REA static GPU code-object reader",
- version: "1",
-} as const;
const OPERATION = "inspect_gpu_code_objects";
/** One admitted descriptor and bounded report at a time; retained cleanup blocks new work. */
diff --git a/src/gpu/GpuCodeObjectsProviderIdentity.ts b/src/gpu/GpuCodeObjectsProviderIdentity.ts
new file mode 100644
index 0000000000..31aff05327
--- /dev/null
+++ b/src/gpu/GpuCodeObjectsProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const GPU_CODE_OBJECTS_PROVIDER_IDENTITY = {
+ id: "gpu-code-objects",
+ name: "REA static GPU code-object reader",
+ version: "1",
+} as const;
diff --git a/src/hopper/HopperProvider.ts b/src/hopper/HopperProvider.ts
index c61e1d0c7e..15f44dc88a 100644
--- a/src/hopper/HopperProvider.ts
+++ b/src/hopper/HopperProvider.ts
@@ -50,10 +50,8 @@ import {
} from "./HopperCallerReconciliation.js";
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
-import {
- hopperCapabilities,
- HOPPER_PROVIDER_IDENTITY,
-} from "./HopperProviderCapabilities.js";
+import { hopperCapabilities } from "./HopperProviderCapabilities.js";
+import { HOPPER_PROVIDER_IDENTITY } from "./HopperProviderMetadata.js";
/** Concrete analysis provider backed by REA's private Hopper bridge. */
export class HopperProvider implements AnalysisProviderCandidate {
diff --git a/src/hopper/HopperProviderCapabilities.ts b/src/hopper/HopperProviderCapabilities.ts
index 487a0b1733..d9c893b1ac 100644
--- a/src/hopper/HopperProviderCapabilities.ts
+++ b/src/hopper/HopperProviderCapabilities.ts
@@ -1,3 +1,7 @@
+import {
+ HOPPER_PROVIDER_IDENTITY,
+ HOPPER_OPERATIONS,
+} from "./HopperProviderMetadata.js";
import {
DEFAULT_REGEX_MATCHING_DEADLINE,
type RegexMatchingDeadline,
@@ -7,58 +11,7 @@ import {
type RegexWorkerHeapRequest,
} from "../config/regexWorkerHeapRequest.js";
import { launchRegexMatchingLimitation } from "../process/RegexMatcher.js";
-import type {
- CapabilityDescriptor,
- ProviderIdentity,
-} from "../application/AnalysisProvider.js";
-import type { OfficialToolName } from "../contracts/officialToolContracts.js";
-
-/** Public identity committed by every Hopper-backed observation. */
-export const HOPPER_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
- id: "hopper",
- name: "Hopper Disassembler",
- version: null,
-});
-
-/** Analyst operations implemented by REA's private Hopper bridge. */
-export const HOPPER_OPERATIONS = Object.freeze([
- "address_name",
- "comment",
- "current_address",
- "current_procedure",
- "current_document",
- "goto_address",
- "inline_comment",
- "list_bookmarks",
- "list_documents",
- "list_names",
- "list_procedures",
- "list_segments",
- "list_strings",
- "next_address",
- "prev_address",
- "procedure_address",
- "procedure_assembly",
- "procedure_callees",
- "procedure_callers",
- "procedure_info",
- "read_function_instructions",
- "read_bytes",
- "address_to_file_offset",
- "procedure_references",
- "procedure_pseudo_code",
- "resolve_containing_procedure",
- "search_procedures",
- "search_strings",
- "set_address_name",
- "set_addresses_names",
- "set_bookmark",
- "set_comment",
- "set_inline_comment",
- "unset_bookmark",
- "xrefs",
- "analyze_function",
-] as const satisfies readonly (OfficialToolName | "analyze_function")[]);
+import type { CapabilityDescriptor } from "../application/AnalysisProvider.js";
const MUTATING_OPERATIONS = new Set([
"set_address_name",
diff --git a/src/hopper/HopperProviderMetadata.ts b/src/hopper/HopperProviderMetadata.ts
new file mode 100644
index 0000000000..0d2f6f8a5b
--- /dev/null
+++ b/src/hopper/HopperProviderMetadata.ts
@@ -0,0 +1,49 @@
+import type { ProviderIdentity } from "../application/AnalysisProvider.js";
+import type { OfficialToolName } from "../contracts/officialToolContracts.js";
+
+/** Public identity committed by every Hopper-backed observation. */
+export const HOPPER_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
+ id: "hopper",
+ name: "Hopper Disassembler",
+ version: null,
+});
+
+/** Analyst operations implemented by REA's private Hopper bridge. */
+export const HOPPER_OPERATIONS = Object.freeze([
+ "address_name",
+ "comment",
+ "current_address",
+ "current_procedure",
+ "current_document",
+ "goto_address",
+ "inline_comment",
+ "list_bookmarks",
+ "list_documents",
+ "list_names",
+ "list_procedures",
+ "list_segments",
+ "list_strings",
+ "next_address",
+ "prev_address",
+ "procedure_address",
+ "procedure_assembly",
+ "procedure_callees",
+ "procedure_callers",
+ "procedure_info",
+ "read_function_instructions",
+ "read_bytes",
+ "address_to_file_offset",
+ "procedure_references",
+ "procedure_pseudo_code",
+ "resolve_containing_procedure",
+ "search_procedures",
+ "search_strings",
+ "set_address_name",
+ "set_addresses_names",
+ "set_bookmark",
+ "set_comment",
+ "set_inline_comment",
+ "unset_bookmark",
+ "xrefs",
+ "analyze_function",
+] as const satisfies readonly (OfficialToolName | "analyze_function")[]);
diff --git a/src/ida/IdaOperationFailures.test.ts b/src/ida/IdaOperationFailures.test.ts
index 45e89f8eab..350af553cd 100644
--- a/src/ida/IdaOperationFailures.test.ts
+++ b/src/ida/IdaOperationFailures.test.ts
@@ -12,7 +12,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { createIdaDiagnosticRedactor } from "./IdaDiagnosticRedactor.js";
import { decodeIdaToolResult } from "./IdaMcpConnection.js";
-import { IDA_PROVIDER_IDENTITY } from "./IdaProviderCapabilities.js";
+import { IDA_PROVIDER_IDENTITY } from "./IdaProviderMetadata.js";
import type { AnalysisOperation } from "../application/AnalysisProvider.js";
import { IdaSessionClient } from "./IdaSessionClient.js";
diff --git a/src/ida/IdaProvider.ts b/src/ida/IdaProvider.ts
index 47730e621a..9dd0594d6f 100644
--- a/src/ida/IdaProvider.ts
+++ b/src/ida/IdaProvider.ts
@@ -42,10 +42,8 @@ import {
createIdaMcpConnection,
type IdaMcpConnection,
} from "./IdaMcpConnection.js";
-import {
- idaCapabilities,
- IDA_PROVIDER_IDENTITY,
-} from "./IdaProviderCapabilities.js";
+import { idaCapabilities } from "./IdaProviderCapabilities.js";
+import { IDA_PROVIDER_IDENTITY } from "./IdaProviderMetadata.js";
import { IdaSessionClient } from "./IdaSessionClient.js";
import { inspectIdaWorkspaceReadiness } from "./IdaWorkspaceReadiness.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
diff --git a/src/ida/IdaProviderCapabilities.test.ts b/src/ida/IdaProviderCapabilities.test.ts
index 607f051543..f533785bf1 100644
--- a/src/ida/IdaProviderCapabilities.test.ts
+++ b/src/ida/IdaProviderCapabilities.test.ts
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import { IDA_COMPATIBILITY_PROFILES } from "./IdaConfiguration.js";
import {
- IDA_OPERATIONS,
idaCapabilities,
idaOperationUnavailableReason,
} from "./IdaProviderCapabilities.js";
+import { IDA_OPERATIONS } from "./IdaProviderMetadata.js";
describe("IDA profile operation admission", () => {
for (const [name, profile] of Object.entries(IDA_COMPATIBILITY_PROFILES)) {
diff --git a/src/ida/IdaProviderCapabilities.ts b/src/ida/IdaProviderCapabilities.ts
index 7fd61be3c2..f999817d24 100644
--- a/src/ida/IdaProviderCapabilities.ts
+++ b/src/ida/IdaProviderCapabilities.ts
@@ -1,34 +1,11 @@
+import {
+ IDA_PROVIDER_IDENTITY,
+ IDA_OPERATIONS,
+} from "./IdaProviderMetadata.js";
import type { IdaCompatibilityProfile } from "./IdaConfiguration.js";
-import type {
- AnalysisOperation,
- CapabilityDescriptor,
-} from "../application/AnalysisProvider.js";
+import type { CapabilityDescriptor } from "../application/AnalysisProvider.js";
import { REGEX_MATCHING_LIMITATION } from "../process/RegexMatcher.js";
-/** This version identifies REA's adapter, not IDA or the upstream MCP server. */
-export const IDA_PROVIDER_IDENTITY = Object.freeze({
- id: "ida",
- name: "IDA MCP adapter",
- version: "1",
-});
-
-/** Read-only analyst outcomes supported by the upstream compatibility profiles. */
-export const IDA_OPERATIONS = Object.freeze([
- "list_procedures",
- "search_procedures",
- "list_strings",
- "search_strings",
- "procedure_address",
- "procedure_pseudo_code",
- "procedure_assembly",
- "procedure_callers",
- "procedure_callees",
- "read_function_instructions",
- "xrefs",
- "analyze_function",
- "resolve_containing_procedure",
- "list_documents",
-] as const satisfies readonly Exclude[]);
export type IdaOperation = (typeof IDA_OPERATIONS)[number];
/** Operations only the modern headless profile implements. */
diff --git a/src/ida/IdaProviderMetadata.ts b/src/ida/IdaProviderMetadata.ts
new file mode 100644
index 0000000000..874ab45f86
--- /dev/null
+++ b/src/ida/IdaProviderMetadata.ts
@@ -0,0 +1,26 @@
+import type { AnalysisOperation } from "../application/AnalysisProvider.js";
+
+/** This version identifies REA's adapter, not IDA or the upstream MCP server. */
+export const IDA_PROVIDER_IDENTITY = Object.freeze({
+ id: "ida",
+ name: "IDA MCP adapter",
+ version: "1",
+});
+
+/** Read-only analyst outcomes supported by the upstream compatibility profiles. */
+export const IDA_OPERATIONS = Object.freeze([
+ "list_procedures",
+ "search_procedures",
+ "list_strings",
+ "search_strings",
+ "procedure_address",
+ "procedure_pseudo_code",
+ "procedure_assembly",
+ "procedure_callers",
+ "procedure_callees",
+ "read_function_instructions",
+ "xrefs",
+ "analyze_function",
+ "resolve_containing_procedure",
+ "list_documents",
+] as const satisfies readonly Exclude[]);
diff --git a/src/ida/IdaSessionClient.ts b/src/ida/IdaSessionClient.ts
index c0740155ea..8b488c4fee 100644
--- a/src/ida/IdaSessionClient.ts
+++ b/src/ida/IdaSessionClient.ts
@@ -32,11 +32,13 @@ import { isX86OnlyTarget } from "./IdaCallGraph.js";
import { IdaOperationRunner } from "./IdaOperationRunner.js";
import {
IDA_LIMITATIONS,
- IDA_OPERATIONS,
- IDA_PROVIDER_IDENTITY,
idaOperationUnavailableReason,
type IdaOperation,
} from "./IdaProviderCapabilities.js";
+import {
+ IDA_OPERATIONS,
+ IDA_PROVIDER_IDENTITY,
+} from "./IdaProviderMetadata.js";
import {
absentDatabaseCloseSchema,
closeDatabaseSchema,
diff --git a/src/il2cpp/Il2CppMetadataProviderIdentity.ts b/src/il2cpp/Il2CppMetadataProviderIdentity.ts
new file mode 100644
index 0000000000..5bf016ad97
--- /dev/null
+++ b/src/il2cpp/Il2CppMetadataProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Bundled format reader identity; the metadata version is a separate observed fact. */
+export const IL2CPP_METADATA_PROVIDER_IDENTITY = {
+ id: "il2cpp-metadata",
+ name: "REA IL2CPP global-metadata reader",
+ version: "1",
+} as const;
diff --git a/src/il2cpp/Il2cppMetadataProvider.ts b/src/il2cpp/Il2cppMetadataProvider.ts
index ec20e8cf85..0cdd5b8e2e 100644
--- a/src/il2cpp/Il2cppMetadataProvider.ts
+++ b/src/il2cpp/Il2cppMetadataProvider.ts
@@ -1,3 +1,4 @@
+import { IL2CPP_METADATA_PROVIDER_IDENTITY } from "./Il2CppMetadataProviderIdentity.js";
import { isSelectedRegularFileError } from "../filesystem/RegularFile.js";
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
import { Il2cppMetadataFormatFailure } from "./Il2cppMetadataFormat.js";
@@ -55,12 +56,6 @@ import {
type Il2cppBinaryLink,
} from "./Il2cppBinaryFile.js";
-/** Bundled format reader identity; the metadata version is a separate observed fact. */
-export const IL2CPP_METADATA_PROVIDER_IDENTITY = {
- id: "il2cpp-metadata",
- name: "REA IL2CPP global-metadata reader",
- version: "1",
-} as const;
const OPERATION = "inspect_il2cpp_metadata";
const INPUT_BYTES = 256 * 1024 * 1024;
// GameAssembly binaries of large games reach several hundred megabytes.
diff --git a/src/javascript/recovery/WakaruExecution.ts b/src/javascript/recovery/WakaruExecution.ts
index bf09845cd3..4e8a41c21c 100644
--- a/src/javascript/recovery/WakaruExecution.ts
+++ b/src/javascript/recovery/WakaruExecution.ts
@@ -25,7 +25,8 @@ import {
} from "./WakaruCommand.js";
import { parseWakaruReports } from "./WakaruReport.js";
import { publishWakaruArtifacts } from "./WakaruPublication.js";
-import { RECOVERY_LIMITS, WAKARU_PROVIDER_IDENTITY } from "./WakaruRelease.js";
+import { RECOVERY_LIMITS } from "./WakaruRelease.js";
+import { WAKARU_PROVIDER_IDENTITY } from "./WakaruProviderIdentity.js";
import { admitWakaruVersion } from "./WakaruVersion.js";
const OPERATION = "recover_javascript_sources";
diff --git a/src/javascript/recovery/WakaruProviderIdentity.ts b/src/javascript/recovery/WakaruProviderIdentity.ts
new file mode 100644
index 0000000000..51d718f9a2
--- /dev/null
+++ b/src/javascript/recovery/WakaruProviderIdentity.ts
@@ -0,0 +1,13 @@
+/** Unchanged upstream source pin; executed bytes are identified separately. */
+export const WAKARU_RELEASE = {
+ version: "1.14.0",
+ revision: "8219bf5016a063d7fb215101a8c45d5959ec733f",
+ repository: "https://github.com/pionxzh/wakaru",
+} as const;
+
+/** Catalog identity for the verified Wakaru release. Evidence replaces version with the observed banner. */
+export const WAKARU_PROVIDER_IDENTITY = {
+ id: "wakaru",
+ name: "Wakaru",
+ version: WAKARU_RELEASE.version,
+} as const;
diff --git a/src/javascript/recovery/WakaruPublication.ts b/src/javascript/recovery/WakaruPublication.ts
index f381118c86..23a5ddea57 100644
--- a/src/javascript/recovery/WakaruPublication.ts
+++ b/src/javascript/recovery/WakaruPublication.ts
@@ -18,11 +18,8 @@ import {
validateRecoverySourceMap,
type parseWakaruReports,
} from "./WakaruReport.js";
-import {
- RECOVERY_LIMITATIONS,
- RECOVERY_LIMITS,
- WAKARU_RELEASE,
-} from "./WakaruRelease.js";
+import { RECOVERY_LIMITATIONS, RECOVERY_LIMITS } from "./WakaruRelease.js";
+import { WAKARU_RELEASE } from "./WakaruProviderIdentity.js";
import type {
resolveWakaruCommand,
runWakaruCommand,
diff --git a/src/javascript/recovery/WakaruRelease.ts b/src/javascript/recovery/WakaruRelease.ts
index 399769d5b0..9153b2d9a3 100644
--- a/src/javascript/recovery/WakaruRelease.ts
+++ b/src/javascript/recovery/WakaruRelease.ts
@@ -1,12 +1,5 @@
import { BOUNDED_PRODUCER_LIMITS } from "../../process/LinuxResourceLimits.js";
-/** Unchanged upstream source pin; executed bytes are identified separately. */
-export const WAKARU_RELEASE = {
- version: "1.14.0",
- revision: "8219bf5016a063d7fb215101a8c45d5959ec733f",
- repository: "https://github.com/pionxzh/wakaru",
-} as const;
-
/**
* Releases the report parser accepts. Wakaru changes `--version`, `--json`
* and `provenance.json` only by addition within a major version
@@ -15,13 +8,6 @@ export const WAKARU_RELEASE = {
*/
export const WAKARU_ACCEPTED_RANGE = "^1.13.0";
-/** Catalog identity for the verified Wakaru release. Evidence replaces version with the observed banner. */
-export const WAKARU_PROVIDER_IDENTITY = {
- id: "wakaru",
- name: "Wakaru",
- version: WAKARU_RELEASE.version,
-} as const;
-
/** Fixed Wakaru allowance. This release has no caller setting that raises it. */
export const wakaruFixedLimitRecovery = (
allowance: string,
diff --git a/src/javascript/recovery/WakaruVersion.test.ts b/src/javascript/recovery/WakaruVersion.test.ts
index 939c2e088b..8305b483c6 100644
--- a/src/javascript/recovery/WakaruVersion.test.ts
+++ b/src/javascript/recovery/WakaruVersion.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { admitWakaruVersion } from "./WakaruVersion.js";
-import { WAKARU_RELEASE } from "./WakaruRelease.js";
+import { WAKARU_RELEASE } from "./WakaruProviderIdentity.js";
describe("Wakaru release range", () => {
it("accepts the verified banner without an extra limitation", () => {
diff --git a/src/javascript/recovery/WakaruVersion.ts b/src/javascript/recovery/WakaruVersion.ts
index f4c2d92ae5..3b99ac7470 100644
--- a/src/javascript/recovery/WakaruVersion.ts
+++ b/src/javascript/recovery/WakaruVersion.ts
@@ -1,5 +1,6 @@
import { satisfies, valid } from "semver";
-import { WAKARU_ACCEPTED_RANGE, WAKARU_RELEASE } from "./WakaruRelease.js";
+import { WAKARU_ACCEPTED_RANGE } from "./WakaruRelease.js";
+import { WAKARU_RELEASE } from "./WakaruProviderIdentity.js";
/** Admission of one observed `wakaru --version` banner. */
export type WakaruVersionAdmission =
diff --git a/src/jeb/JebProvider.ts b/src/jeb/JebProvider.ts
index ab085d7a92..235609d258 100644
--- a/src/jeb/JebProvider.ts
+++ b/src/jeb/JebProvider.ts
@@ -1,3 +1,4 @@
+import { JEB_PROVIDER_IDENTITY } from "./JebProviderIdentity.js";
import {
JebMcpAcquisitionCleanupFailure,
JebMcpPhysicalFailure,
@@ -41,12 +42,6 @@ import {
const JEB_CONNECTION_REMEDIATION = `Start a JEB client serving MCP (default ${DEFAULT_JEB_MCP_ENDPOINT}) or set REA_JEB_MCP_URL to the running endpoint.`;
-export const JEB_PROVIDER_IDENTITY: ProviderIdentity = {
- id: "jeb",
- name: "JEB",
- version: null,
-} as const;
-
export interface JebProviderOptions {
/** Caller environment carrying REA_JEB_MCP_URL. */
readonly environment?: Readonly>;
diff --git a/src/jeb/JebProviderIdentity.ts b/src/jeb/JebProviderIdentity.ts
new file mode 100644
index 0000000000..3d75f7ca89
--- /dev/null
+++ b/src/jeb/JebProviderIdentity.ts
@@ -0,0 +1,8 @@
+import type { ProviderIdentity } from "../application/AnalysisProvider.js";
+
+/** Source-owned static provider identity and public operation names. */
+export const JEB_PROVIDER_IDENTITY: ProviderIdentity = {
+ id: "jeb",
+ name: "JEB",
+ version: null,
+} as const;
diff --git a/src/kuna/KunaProvider.ts b/src/kuna/KunaProvider.ts
index c1ade5afcd..2d8a408906 100644
--- a/src/kuna/KunaProvider.ts
+++ b/src/kuna/KunaProvider.ts
@@ -40,10 +40,8 @@ import {
inspectKunaInstallation,
} from "./KunaInstallation.js";
import { KunaProjectCache } from "./KunaProjectCache.js";
-import {
- KUNA_PROVIDER_IDENTITY,
- kunaCapabilities,
-} from "./KunaProviderCapabilities.js";
+import { kunaCapabilities } from "./KunaProviderCapabilities.js";
+import { KUNA_PROVIDER_IDENTITY } from "./KunaProviderMetadata.js";
import { KUNA_COMMAND_REMEDIATION, KUNA_PROVIDER_ID } from "./KunaRelease.js";
import { KunaRunSlots } from "./KunaRunSlots.js";
import { KunaSessionClient } from "./KunaSessionClient.js";
diff --git a/src/kuna/KunaProviderCapabilities.ts b/src/kuna/KunaProviderCapabilities.ts
index 425b5fdf6a..1b325265a2 100644
--- a/src/kuna/KunaProviderCapabilities.ts
+++ b/src/kuna/KunaProviderCapabilities.ts
@@ -1,9 +1,13 @@
+import {
+ KUNA_PROVIDER_IDENTITY,
+ KUNA_OPERATIONS,
+} from "./KunaProviderMetadata.js";
import type {
AnalysisOperation,
CapabilityDescriptor,
} from "../application/AnalysisProvider.js";
import type { ProviderRejectionCode } from "../domain/providerSelection.js";
-import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
+
import {
DEFAULT_REGEX_MATCHING_DEADLINE,
type RegexMatchingDeadline,
@@ -15,29 +19,6 @@ import {
import { launchRegexMatchingLimitation } from "../process/RegexMatcher.js";
import { KUNA_FUNCTION_TEXT_BYTES } from "./KunaLimits.js";
-/** Static identity; the observed Kuna release is reported in each analysis profile. */
-export const KUNA_PROVIDER_IDENTITY = Object.freeze({
- id: KUNA_PROVIDER_ID,
- name: "Kuna",
- version: null,
-});
-
-/** Analyst outcomes Kuna's inventory and project export can answer. */
-export const KUNA_OPERATIONS = Object.freeze([
- "list_documents",
- "list_procedures",
- "search_procedures",
- "list_strings",
- "search_strings",
- "procedure_address",
- "address_name",
- "procedure_pseudo_code",
- "procedure_assembly",
- "analyze_function",
- "procedure_callers",
- "procedure_callees",
- "xrefs",
-] as const satisfies readonly Exclude[]);
export type KunaOperation = (typeof KUNA_OPERATIONS)[number];
export const KUNA_RELATIONSHIP_LIMITATION =
diff --git a/src/kuna/KunaProviderMetadata.ts b/src/kuna/KunaProviderMetadata.ts
new file mode 100644
index 0000000000..54b38571b7
--- /dev/null
+++ b/src/kuna/KunaProviderMetadata.ts
@@ -0,0 +1,26 @@
+import type { AnalysisOperation } from "../application/AnalysisProvider.js";
+import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
+
+/** Static identity; the observed Kuna release is reported in each analysis profile. */
+export const KUNA_PROVIDER_IDENTITY = Object.freeze({
+ id: KUNA_PROVIDER_ID,
+ name: "Kuna",
+ version: null,
+});
+
+/** Analyst outcomes Kuna's inventory and project export can answer. */
+export const KUNA_OPERATIONS = Object.freeze([
+ "list_documents",
+ "list_procedures",
+ "search_procedures",
+ "list_strings",
+ "search_strings",
+ "procedure_address",
+ "address_name",
+ "procedure_pseudo_code",
+ "procedure_assembly",
+ "analyze_function",
+ "procedure_callers",
+ "procedure_callees",
+ "xrefs",
+] as const satisfies readonly Exclude[]);
diff --git a/src/main/transport.ts b/src/main/transport.ts
index 290bd9b7ad..8090ae613a 100644
--- a/src/main/transport.ts
+++ b/src/main/transport.ts
@@ -134,6 +134,11 @@ export const startMcpTransport = async (
serverContext.analysisSettings?.read,
);
androidProviders.add(android);
+ const closeAndroidProvider = android.close.bind(android);
+ android.close = async () => {
+ await closeAndroidProvider();
+ androidProviders.delete(android);
+ };
const server = (dependencies.createServer ?? createServer)(
{ kind: "session", session },
{
diff --git a/src/model/ModelArtifactProvider.ts b/src/model/ModelArtifactProvider.ts
index a696bab531..fe8d0c7d56 100644
--- a/src/model/ModelArtifactProvider.ts
+++ b/src/model/ModelArtifactProvider.ts
@@ -1,3 +1,4 @@
+import { MODEL_ARTIFACT_PROVIDER_IDENTITY } from "./ModelArtifactProviderIdentity.js";
import { lstat, type FileHandle } from "node:fs/promises";
import type { ModelArtifactPort } from "../application/model/ModelArtifactPort.js";
import {
@@ -51,11 +52,6 @@ import {
sameRegularFileState,
} from "../filesystem/RegularFile.js";
-export const MODEL_ARTIFACT_PROVIDER_IDENTITY = {
- id: "model-artifact",
- name: "REA model header reader",
- version: "1",
-} as const;
const OPERATION = "inspect_model_artifact";
/** One admitted descriptor and bounded report at a time; retained cleanup blocks new work. */
export class ModelArtifactProvider implements ModelArtifactPort {
diff --git a/src/model/ModelArtifactProviderIdentity.ts b/src/model/ModelArtifactProviderIdentity.ts
new file mode 100644
index 0000000000..d2d0ee530a
--- /dev/null
+++ b/src/model/ModelArtifactProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const MODEL_ARTIFACT_PROVIDER_IDENTITY = {
+ id: "model-artifact",
+ name: "REA model header reader",
+ version: "1",
+} as const;
diff --git a/src/native/AppleDispatchMetadata.ts b/src/native/AppleDispatchMetadata.ts
index f064327444..405ccaf7e8 100644
--- a/src/native/AppleDispatchMetadata.ts
+++ b/src/native/AppleDispatchMetadata.ts
@@ -34,10 +34,8 @@ import { AnalysisError } from "../domain/analysisErrorBase.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
-import {
- APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE,
- NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY,
-} from "./NativeMacOSProviderMetadata.js";
+import { APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE } from "./NativeMacOSProviderMetadata.js";
+import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js";
import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
import { pushDispatchCoverage } from "./AppleDispatchCoverage.js";
diff --git a/src/native/NativeCallTarget.ts b/src/native/NativeCallTarget.ts
index e55fd669fa..9f85c0731f 100644
--- a/src/native/NativeCallTarget.ts
+++ b/src/native/NativeCallTarget.ts
@@ -1,4 +1,4 @@
-import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacOSProviderMetadata.js";
+import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
diff --git a/src/native/NativeCommandAnalysisError.ts b/src/native/NativeCommandAnalysisError.ts
index a6ff201378..69fd40efad 100644
--- a/src/native/NativeCommandAnalysisError.ts
+++ b/src/native/NativeCommandAnalysisError.ts
@@ -6,7 +6,7 @@ import {
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
-import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacOSProviderMetadata.js";
+import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js";
import {
DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
fixedNativeAllowance,
diff --git a/src/native/NativeMacOSProvider.ts b/src/native/NativeMacOSProvider.ts
index 17533d8955..9269614512 100644
--- a/src/native/NativeMacOSProvider.ts
+++ b/src/native/NativeMacOSProvider.ts
@@ -23,9 +23,9 @@ import { readMachoEncryption } from "../artifacts/apple/MachoEncryptionReader.js
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import {
APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE,
- NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY,
nativeMacOSCapabilities,
} from "./NativeMacOSProviderMetadata.js";
+import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js";
import {
appleDispatchMetadataFailure,
inspectAppleDispatchMetadata,
diff --git a/src/native/NativeMacOSProviderMetadata.ts b/src/native/NativeMacOSProviderMetadata.ts
index 2722e6c795..6c125f25f5 100644
--- a/src/native/NativeMacOSProviderMetadata.ts
+++ b/src/native/NativeMacOSProviderMetadata.ts
@@ -1,16 +1,8 @@
-import type {
- CapabilityDescriptor,
- ProviderIdentity,
-} from "../application/AnalysisProvider.js";
+import { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacosProviderIdentity.js";
+import type { CapabilityDescriptor } from "../application/AnalysisProvider.js";
import { createAnalysisProfile } from "../domain/analysisProfile.js";
import { NATIVE_TOOL_CONTRACTS } from "../contracts/native/nativeToolContracts.js";
-/** Public identity committed by macOS-native inspection observations. */
-export const NATIVE_MACOS_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
- id: "native-macos",
- name: "macOS native inspection utilities",
- version: null,
-});
/** Source-owned commitment for the pure Apple dispatch metadata reader. */
export const APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE = createAnalysisProfile(
{
diff --git a/src/native/NativeMacosProviderIdentity.ts b/src/native/NativeMacosProviderIdentity.ts
new file mode 100644
index 0000000000..317d20c684
--- /dev/null
+++ b/src/native/NativeMacosProviderIdentity.ts
@@ -0,0 +1,8 @@
+import type { ProviderIdentity } from "../application/AnalysisProvider.js";
+
+/** Public identity committed by macOS-native inspection observations. */
+export const NATIVE_MACOS_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
+ id: "native-macos",
+ name: "macOS native inspection utilities",
+ version: null,
+});
diff --git a/src/native/NativePlistSource.ts b/src/native/NativePlistSource.ts
index fbdb628a1a..83f0a1ab7a 100644
--- a/src/native/NativePlistSource.ts
+++ b/src/native/NativePlistSource.ts
@@ -36,7 +36,7 @@ import {
withArtifactRegularFile,
} from "../artifacts/ArtifactFileOperation.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
-import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacOSProviderMetadata.js";
+import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js";
import {
MAX_NATIVE_PLIST_BYTES,
NATIVE_PLIST_INPUT_ALLOWANCE,
diff --git a/src/native/SignatureTargetBinding.ts b/src/native/SignatureTargetBinding.ts
index 8d7b4d6335..ca26b1dd63 100644
--- a/src/native/SignatureTargetBinding.ts
+++ b/src/native/SignatureTargetBinding.ts
@@ -31,7 +31,7 @@ import {
import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js";
import type { ArtifactResourceOwner } from "../artifacts/ArtifactResourceScope.js";
-import { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacOSProviderMetadata.js";
+import { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacosProviderIdentity.js";
type SignatureOwner = Extract<
ArtifactResourceOwner,
diff --git a/src/native/pwntools/PwntoolsDecoder.ts b/src/native/pwntools/PwntoolsDecoder.ts
index ae3342e5fe..60b8bc967a 100644
--- a/src/native/pwntools/PwntoolsDecoder.ts
+++ b/src/native/pwntools/PwntoolsDecoder.ts
@@ -65,10 +65,8 @@ import {
readPwntoolsFailureEvidence,
} from "./PwntoolsResourceLimits.js";
import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js";
-import {
- PWNTOOLS_PROVIDER_IDENTITY,
- PWNTOOLS_LIMITS,
-} from "./PwntoolsRelease.js";
+import { PWNTOOLS_LIMITS } from "./PwntoolsRelease.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js";
import { pwntoolsInputBytes } from "./PwntoolsInputLimit.js";
const HOST_REQUIREMENT =
diff --git a/src/native/pwntools/PwntoolsFailures.ts b/src/native/pwntools/PwntoolsFailures.ts
index 4529f82e3e..10da1660c5 100644
--- a/src/native/pwntools/PwntoolsFailures.ts
+++ b/src/native/pwntools/PwntoolsFailures.ts
@@ -25,11 +25,11 @@ import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
import type { PwntoolsFailureEvidence } from "./PwntoolsResourceLimits.js";
import { nativeDeadlineRecovery } from "../../domain/native/nativeDeadlines.js";
import {
- PWNTOOLS_PROVIDER_IDENTITY,
PWNTOOLS_LIMITS,
PWNTOOLS_MEMORY_FAILURE_EXIT,
PWNTOOLS_FILE_SIZE_FAILURE_EXIT,
} from "./PwntoolsRelease.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js";
/** Operation identity supplied by each adapter sharing the pwntools boundary. */
export interface PwntoolsFailureContext {
diff --git a/src/native/pwntools/PwntoolsInputLimit.test.ts b/src/native/pwntools/PwntoolsInputLimit.test.ts
index d60c27b801..2595c03c50 100644
--- a/src/native/pwntools/PwntoolsInputLimit.test.ts
+++ b/src/native/pwntools/PwntoolsInputLimit.test.ts
@@ -2,10 +2,8 @@ import { describe, expect, it } from "vitest";
import { pwntoolsInputBytes } from "./PwntoolsInputLimit.js";
import { pwntoolsUnavailable } from "./PwntoolsFailures.js";
-import {
- PWNTOOLS_LIMITS,
- PWNTOOLS_PROVIDER_IDENTITY,
-} from "./PwntoolsRelease.js";
+import { PWNTOOLS_LIMITS } from "./PwntoolsRelease.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js";
const CONSTRAINT =
"REA_PWNTOOLS_MAX_INPUT_BYTES must be a decimal integer from 1 to " +
diff --git a/src/native/pwntools/PwntoolsLayoutProvider.ts b/src/native/pwntools/PwntoolsLayoutProvider.ts
index f9346e397b..6e42ea1b42 100644
--- a/src/native/pwntools/PwntoolsLayoutProvider.ts
+++ b/src/native/pwntools/PwntoolsLayoutProvider.ts
@@ -6,7 +6,7 @@ import {
} from "../../domain/native/binaryLayout.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { PwntoolsDecoder, type PwntoolsLauncher } from "./PwntoolsDecoder.js";
-import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsRelease.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js";
import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js";
import type { BinaryLayoutPort } from "../../application/binaryDiagnostics/BinaryLayoutPort.js";
import type { z } from "zod";
diff --git a/src/native/pwntools/PwntoolsProviderIdentity.ts b/src/native/pwntools/PwntoolsProviderIdentity.ts
new file mode 100644
index 0000000000..6cbb90a329
--- /dev/null
+++ b/src/native/pwntools/PwntoolsProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Verified unchanged upstream profile for the Linux ELF adapter. */
+export const PWNTOOLS_PROVIDER_IDENTITY = {
+ id: "pwntools-elf",
+ name: "REA pwntools ELF adapter",
+ version: "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2",
+} as const;
diff --git a/src/native/pwntools/PwntoolsRecordedCrashProvider.ts b/src/native/pwntools/PwntoolsRecordedCrashProvider.ts
index 12d8f6408e..92fd9c1bc4 100644
--- a/src/native/pwntools/PwntoolsRecordedCrashProvider.ts
+++ b/src/native/pwntools/PwntoolsRecordedCrashProvider.ts
@@ -13,7 +13,7 @@ import {
import type { RecordedCrashPort } from "../../application/binaryDiagnostics/RecordedCrashPort.js";
import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js";
import { PwntoolsDecoder, type PwntoolsLauncher } from "./PwntoolsDecoder.js";
-import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsRelease.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js";
import { validateCoreFileMappingAuthority } from "../pwndbg/CoreFileMappingAuthority.js";
import { validateRecordedCrashSources } from "./RecordedCrashSourceBindings.js";
import { validateRecordedCrashStructure } from "./RecordedCrashStructureBindings.js";
diff --git a/src/native/pwntools/PwntoolsRelease.ts b/src/native/pwntools/PwntoolsRelease.ts
index 67bd73bd2b..b52baeb367 100644
--- a/src/native/pwntools/PwntoolsRelease.ts
+++ b/src/native/pwntools/PwntoolsRelease.ts
@@ -1,11 +1,5 @@
import { DEFAULT_PWNTOOLS_TIMEOUT_MS } from "../../domain/native/nativeDeadlines.js";
-/** Verified unchanged upstream profile for the Linux ELF adapter. */
-export const PWNTOOLS_PROVIDER_IDENTITY = {
- id: "pwntools-elf",
- name: "REA pwntools ELF adapter",
- version: "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2",
-} as const;
/** Bridge-reserved status for MemoryError when no structured reply can be written. */
export const PWNTOOLS_MEMORY_FAILURE_EXIT = 75;
/** Bridge-reserved status for an observed EFBIG failure without a complete reply. */
diff --git a/src/objdump/ObjdumpCommand.ts b/src/objdump/ObjdumpCommand.ts
index ae899672c9..40f49faabb 100644
--- a/src/objdump/ObjdumpCommand.ts
+++ b/src/objdump/ObjdumpCommand.ts
@@ -1,12 +1,6 @@
import type { ObjdumpSelection } from "../domain/reverseEngineering.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
-export const OBJDUMP_PROVIDER_IDENTITY = {
- id: "gnu.objdump",
- name: "GNU objdump",
- version: null,
-} as const;
-
/** Build GNU objdump argv without invoking a shell or following DWARF links by default. */
export const objdumpCommand = (input: ObjdumpSelection): readonly string[] => {
if (input.operation === "architectures") return ["-i"];
diff --git a/src/objdump/ObjdumpProviderIdentity.ts b/src/objdump/ObjdumpProviderIdentity.ts
new file mode 100644
index 0000000000..aa4d074b42
--- /dev/null
+++ b/src/objdump/ObjdumpProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const OBJDUMP_PROVIDER_IDENTITY = {
+ id: "gnu.objdump",
+ name: "GNU objdump",
+ version: null,
+} as const;
diff --git a/src/pcap/PcapProvider.ts b/src/pcap/PcapProvider.ts
index 521c9ba6ec..7cee0ed5c8 100644
--- a/src/pcap/PcapProvider.ts
+++ b/src/pcap/PcapProvider.ts
@@ -1,3 +1,4 @@
+import { PCAP_PROVIDER_IDENTITY } from "./PcapProviderIdentity.js";
import { PCAP_HTTP_URI_FIELDS } from "./PcapHttpUri.js";
import {
type ExecutionOptions,
@@ -67,11 +68,6 @@ import {
type ArtifactCommandPath,
} from "../process/ArtifactCommandPath.js";
-export const PCAP_PROVIDER_IDENTITY = {
- id: "pcap",
- name: "Packet capture analysis",
- version: null,
-} as const;
const FIELDS = [
"frame.number",
"frame.protocols",
diff --git a/src/pcap/PcapProviderIdentity.ts b/src/pcap/PcapProviderIdentity.ts
new file mode 100644
index 0000000000..a0276e8b25
--- /dev/null
+++ b/src/pcap/PcapProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const PCAP_PROVIDER_IDENTITY = {
+ id: "pcap",
+ name: "Packet capture analysis",
+ version: null,
+} as const;
diff --git a/src/python/PythonPycProvider.ts b/src/python/PythonPycProvider.ts
index 18f7c0b278..a95fc5cb6c 100644
--- a/src/python/PythonPycProvider.ts
+++ b/src/python/PythonPycProvider.ts
@@ -1,3 +1,4 @@
+import { PYTHON_PYC_PROVIDER_IDENTITY } from "./PythonPycProviderIdentity.js";
import {
projectStableArtifactFailure,
projectArtifactCloseFailure,
@@ -34,13 +35,6 @@ import {
} from "./PythonPyc.errors.js";
import { parsePythonPyc } from "./PythonPycParser.js";
-/** Bundled reader identity. Header python_version is a separate observed mapping. */
-export const PYTHON_PYC_PROVIDER_IDENTITY = {
- id: "rea-python-pyc",
- name: "REA CPython pyc parser",
- version: "1",
-} as const;
-
const OPERATION = "inspect_python_pyc";
const parserLimitRecovery = (
diff --git a/src/python/PythonPycProviderIdentity.ts b/src/python/PythonPycProviderIdentity.ts
new file mode 100644
index 0000000000..a5a90ca475
--- /dev/null
+++ b/src/python/PythonPycProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Bundled reader identity. Header python_version is a separate observed mapping. */
+export const PYTHON_PYC_PROVIDER_IDENTITY = {
+ id: "rea-python-pyc",
+ name: "REA CPython pyc parser",
+ version: "1",
+} as const;
diff --git a/src/rizin/RizinCommand.ts b/src/rizin/RizinCommand.ts
index df45fb61da..c571a29c2f 100644
--- a/src/rizin/RizinCommand.ts
+++ b/src/rizin/RizinCommand.ts
@@ -1,12 +1,6 @@
import type { RizinInput } from "../domain/reverseEngineering.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
-export const RIZIN_PROVIDER_IDENTITY = {
- id: "rizin",
- name: "Rizin",
- version: null,
-} as const;
-
/** Default one-shot presentation to plain text while retaining plugin availability. */
export const rizinCommand = (input: RizinInput): readonly string[] => [
"-N",
diff --git a/src/rizin/RizinDebugSessionManager.ts b/src/rizin/RizinDebugSessionManager.ts
index 050d9eb349..95766ac089 100644
--- a/src/rizin/RizinDebugSessionManager.ts
+++ b/src/rizin/RizinDebugSessionManager.ts
@@ -1,3 +1,4 @@
+import { RIZIN_DEBUGGER_PROVIDER_IDENTITY } from "./RizinDebuggerProviderIdentity.js";
import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js";
import { randomUUID } from "node:crypto";
import { isUtf8 } from "node:buffer";
@@ -54,12 +55,6 @@ const MAX_COMMAND_FRAMES = 4096;
const MAX_RECENT_FRAME_BYTES = 16 * 1024;
const MAX_DIAGNOSTIC_BYTES = 64 * 1024;
-export const RIZIN_DEBUGGER_PROVIDER_IDENTITY = {
- id: "rizin.debugger",
- name: "Rizin debugger",
- version: null,
-} as const;
-
interface RizinLaunchProvenance {
readonly caller_selected_path: string;
readonly resolved_path: string;
diff --git a/src/rizin/RizinDebuggerProviderIdentity.ts b/src/rizin/RizinDebuggerProviderIdentity.ts
new file mode 100644
index 0000000000..969d563383
--- /dev/null
+++ b/src/rizin/RizinDebuggerProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const RIZIN_DEBUGGER_PROVIDER_IDENTITY = {
+ id: "rizin.debugger",
+ name: "Rizin debugger",
+ version: null,
+} as const;
diff --git a/src/rizin/RizinProviderIdentity.ts b/src/rizin/RizinProviderIdentity.ts
new file mode 100644
index 0000000000..003c3ebe40
--- /dev/null
+++ b/src/rizin/RizinProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const RIZIN_PROVIDER_IDENTITY = {
+ id: "rizin",
+ name: "Rizin",
+ version: null,
+} as const;
diff --git a/src/rust/RustBinaryProvider.ts b/src/rust/RustBinaryProvider.ts
index c6712c5c2a..c80443d4c7 100644
--- a/src/rust/RustBinaryProvider.ts
+++ b/src/rust/RustBinaryProvider.ts
@@ -1,3 +1,4 @@
+import { RUST_BINARY_PROVIDER_IDENTITY } from "./RustBinaryProviderIdentity.js";
import {
projectStableArtifactFailure,
projectArtifactCloseFailure,
@@ -45,12 +46,6 @@ import {
RustAuditableResourceFailure,
} from "./RustAuditableReader.js";
-/** Bundled reader identity. Embedded package versions are separate observed facts. */
-export const RUST_BINARY_PROVIDER_IDENTITY = {
- id: "rust-auditable",
- name: "REA Rust dependency reader",
- version: "1",
-} as const;
const OPERATION = "inspect_rust_binary";
/** Stable in-process dependency inspection without subprocesses, temporary files, or a Rust toolchain. */
diff --git a/src/rust/RustBinaryProviderIdentity.ts b/src/rust/RustBinaryProviderIdentity.ts
new file mode 100644
index 0000000000..3023bf5328
--- /dev/null
+++ b/src/rust/RustBinaryProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Bundled reader identity. Embedded package versions are separate observed facts. */
+export const RUST_BINARY_PROVIDER_IDENTITY = {
+ id: "rust-auditable",
+ name: "REA Rust dependency reader",
+ version: "1",
+} as const;
diff --git a/src/server/createServer.ts b/src/server/createServer.ts
index c12b5e5a52..5808bd1ba0 100644
--- a/src/server/createServer.ts
+++ b/src/server/createServer.ts
@@ -1,47 +1,21 @@
+import {
+ createCapabilityServices,
+ type CapabilityServiceOptions,
+} from "../composition/capabilityServices.js";
import { registerToolFamilies } from "../contracts/toolFamilyIds.js";
import { AnalysisSettings } from "../application/AnalysisSettings.js";
import { parseMcpInputBudget } from "../config/mcpInputBudget.js";
import { registerAnalysisSettingsTools } from "./registerAnalysisSettingsTools.js";
import { registerDevicetreeTool } from "./registerDevicetreeTool.js";
-import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js";
-import { createGpuCodeObjectsService } from "../composition/gpu.js";
import { registerGpuCodeObjectsTools } from "./registerGpuCodeObjectsTools.js";
-import type { PacketCaptureService } from "../application/captures/PacketCaptureService.js";
import { serverInstructions } from "./serverInstructions.js";
-import {
- createPacketCaptureService,
- tsharkHostSupported,
-} from "../composition/captures.js";
import { registerCaptureTools } from "./registerCaptureTools.js";
-import type { SignalRecordingService } from "../application/signals/SignalRecordingService.js";
-import {
- createSignalRecordingService,
- sigrokHostSupported,
-} from "../composition/signals.js";
import { registerSignalTools } from "./registerSignalTools.js";
-import type { WasmArtifactService } from "../application/wasm/WasmArtifactService.js";
-import { wasmArtifactModule } from "../composition/wasm.js";
import { registerWasmTools } from "./registerWasmTools.js";
-import type { ApkSignatureService } from "../application/apkSignature/ApkSignatureService.js";
-import { apkSignatureModule } from "../composition/apkSignature.js";
import { registerApkSignatureTools } from "./registerApkSignatureTools.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { captureProviderEnvironment } from "./providerEnvironment.js";
-import {
- createElfNotesService,
- createAppleIpsService,
- createDevicetreeService,
- createKernelModuleService,
- createPeLoadConfigService,
- createPeCertificatesService,
- createPeImportsService,
- createNativeExportsService,
- createPeResourcesService,
- createRecordedCrashService,
- createByteEntropyService,
- createBinaryLayoutService,
- pwntoolsHostSupported,
-} from "../composition/binaryDiagnostics.js";
+import { createByteEntropyService } from "../composition/binaryDiagnostics.js";
import { registerElfNotesTool } from "./registerElfNotesTool.js";
import { registerAppleIpsTool } from "./registerAppleIpsTool.js";
import { registerKernelModuleTool } from "./registerKernelModuleTool.js";
@@ -50,44 +24,19 @@ import { registerPeCertificatesTool } from "./registerPeCertificatesTool.js";
import { registerPeImportsTool } from "./registerPeImportsTool.js";
import { registerPeResourcesTool } from "./registerPeResourcesTool.js";
import { registerNativeExportsTool } from "./registerNativeExportsTool.js";
-import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
-import {
- evmHostSupported,
- evmInterfaceModule,
- evmLimiterAvailability,
-} from "../composition/evm.js";
import { registerEvmTools } from "./registerEvmTools.js";
-import type { SqliteDatabaseService } from "../application/sqlite/SqliteDatabaseService.js";
-import { createSqliteDatabaseService } from "../composition/sqlite.js";
import { registerSqliteTools } from "./registerSqliteTools.js";
import { registerRecordedCrashTools } from "./registerRecordedCrashTools.js";
-import { createDesktopApplicationService } from "../composition/desktop.js";
import { registerDesktopApplicationTool } from "./registerDesktopApplicationTool.js";
-import { createGoBinaryService } from "../composition/go.js";
-import { createModelArtifactService } from "../composition/model.js";
import { registerModelArtifactTools } from "./registerModelArtifactTools.js";
-import { createIl2cppMetadataService } from "../composition/il2cpp.js";
-import type { Il2cppMetadataService } from "../application/il2cpp/Il2cppMetadataService.js";
-import type { GoBinaryService } from "../application/go/GoBinaryService.js";
-import { createElectronFuseService } from "../composition/electronFuses.js";
-import type { ElectronFuseService } from "../application/electron/ElectronFuseService.js";
import { registerGoTools } from "./registerGoTools.js";
import { registerIl2cppTools } from "./registerIl2cppTools.js";
-import { createRustBinaryService } from "../composition/rust.js";
-import type { RustBinaryService } from "../application/rust/RustBinaryService.js";
import { registerRustTools } from "./registerRustTools.js";
-import { createPythonPycService } from "../composition/pythonPyc.js";
-import type { PythonPycService } from "../application/python/PythonPycService.js";
import { registerPythonPycTool } from "./registerPythonPycTool.js";
-import { createBeamService } from "../composition/erlang.js";
-import type { BeamService } from "../application/erlang/BeamService.js";
import { registerErlangTools } from "./registerErlangTools.js";
-import type { RecordedCrashService } from "../application/binaryDiagnostics/RecordedCrashService.js";
import { registerAnalysisViewTool } from "./registerAnalysisViewTool.js";
import { registerBinaryDiagnosticsTools } from "./registerBinaryDiagnosticsTools.js";
-import { FridaInstrumentationManager } from "../frida/FridaInstrumentationManager.js";
import { registerFridaTools } from "./registerFridaTools.js";
-import type { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js";
import { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import { catalogIdentityForToolFamilies } from "../catalogIdentity.js";
import { parseMcpToolFamilies } from "../config/mcpToolFamilies.js";
@@ -103,16 +52,7 @@ import {
parseMcpInputSchemaProfile,
} from "../config/mcpInputSchemaProfile.js";
import type { CompactInputSchemaPresentation } from "../contracts/compactInputSchema.js";
-import { isAbsolute } from "node:path";
-
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
-import type { BrowserObservationPort } from "../application/BrowserObservationPort.js";
-import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
-import type { ElectronActiveObservationPort } from "../application/javascript/ElectronActiveObservationPort.js";
-import type { ElectronObservationPort } from "../application/javascript/ElectronObservationPort.js";
-import type { JavaScriptRuntimeObservationPort } from "../application/javascript/JavaScriptRuntimeObservationPort.js";
-import type { FridaInstrumentationPort } from "../application/frida/FridaInstrumentationPort.js";
-import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import { silentLogger } from "../logger.js";
import type { Logger } from "pino";
@@ -123,36 +63,16 @@ import { registerBrowserScenarioTool } from "./registerBrowserScenarioTool.js";
import { registerBrowserTools } from "./registerBrowserTools.js";
import { registerWebScriptTool } from "./registerWebScriptTool.js";
import { registerWebModuleTool } from "./registerWebModuleTool.js";
-import type { WebModuleTraceService } from "../application/WebModuleTraceService.js";
-import { createWebModuleTraceService } from "../composition/webModules.js";
-import { createWebSourceLocationService } from "../composition/webSourceLocations.js";
import { registerWebSourceLocationTool } from "./registerWebSourceLocationTool.js";
-import type { WebSourceLocationService } from "../application/WebSourceLocationService.js";
-import type { WebRuntimeService } from "../application/WebRuntimeService.js";
-import { createWebRuntimeService } from "../composition/webRuntime.js";
import { registerWebRuntimeTools } from "./registerWebRuntimeTools.js";
-import type { WebNetworkCaptureService } from "../application/WebNetworkCaptureService.js";
-import { createWebNetworkCaptureService } from "../composition/webNetworkCaptures.js";
import { registerWebNetworkCaptureTool } from "./registerWebNetworkCaptureTool.js";
import { registerJavaScriptRecoveryTool } from "./registerJavaScriptRecoveryTool.js";
import { JavaScriptRecoveryService } from "../application/javascript/JavaScriptRecoveryService.js";
-import type { JavaScriptRecoveryPort } from "../application/javascript/JavaScriptRecoveryPort.js";
-import {
- createJavaScriptRecoveryProvider,
- wakaruHostSupported,
-} from "../composition/javascriptRecovery.js";
import { registerElectronTools } from "./registerElectronTools.js";
-import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { registerEnhancedTools } from "./registerEnhancedTools.js";
import { registerJavaScriptRuntimeObservationTools } from "./registerJavaScriptRuntimeObservationTools.js";
import { registerManagedTools } from "./registerManagedTools.js";
import { registerFirmwareTools } from "./registerFirmwareTools.js";
-import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js";
-import type { FirmwareAnalysisPort } from "../application/firmware/FirmwareAnalysisPort.js";
-import {
- createFirmwareAnalysisProvider,
- firmwareHostSupported,
-} from "../composition/firmware.js";
import { registerAndroidTools } from "./registerAndroidTools.js";
import { registerApktoolTools } from "./registerApktoolTools.js";
import { registerPcapTools } from "./registerPcapTools.js";
@@ -161,26 +81,10 @@ import { registerJebTools } from "./registerJebTools.js";
import { registerAdbTools } from "./registerAdbTools.js";
import { AndroidAnalysisService } from "../application/android/AndroidAnalysisService.js";
import { JebAnalysisService } from "../application/jeb/JebAnalysisService.js";
-import type { AndroidAnalysisPort } from "../application/android/AndroidAnalysisPort.js";
-import type { ApktoolResourceAnalysisPort } from "../application/apktool/ApktoolResourceAnalysisPort.js";
-import type { PcapAnalysisPort } from "../application/pcap/PcapAnalysisPort.js";
import { PcapAnalysisService } from "../application/pcap/PcapAnalysisService.js";
-import {
- locateWiresharkTool,
- selectWiresharkTool,
-} from "../pcap/PcapConfiguration.js";
import { ApktoolResourceAnalysisService } from "../application/apktool/ApktoolResourceAnalysisService.js";
-import type { FlutterBuildAnalysisPort } from "../application/flutter/FlutterBuildAnalysisPort.js";
import { FlutterBuildAnalysisService } from "../application/flutter/FlutterBuildAnalysisService.js";
-import { createAndroidAnalysisProvider } from "../composition/android.js";
-import { createApktoolResourceAnalysisProvider } from "../composition/apktool.js";
-import { createPcapAnalysisProvider } from "../composition/pcap.js";
-import { createFlutterBuildAnalysisProvider } from "../composition/flutter.js";
-import type { JebAnalysisPort } from "../application/jeb/JebAnalysisPort.js";
-import { createJebAnalysisProvider } from "../composition/jeb.js";
-import type { AdbDeviceAnalysisPort } from "../application/adb/AdbDeviceAnalysisPort.js";
import { AdbDeviceAnalysisService } from "../application/adb/AdbDeviceAnalysisService.js";
-import { createAdbDeviceAnalysisProvider } from "../composition/adb.js";
import { registerManagedWorkflowTools } from "./registerManagedWorkflowTools.js";
import { registerOfficialTools } from "./registerOfficialTools.js";
import { registerGuidedPrompts } from "./registerPrompts.js";
@@ -191,18 +95,12 @@ import {
withAdmittedAnalysis,
type ServerAnalysisSource,
} from "./analysisAdmission.js";
-import { createReverseEngineeringService } from "../composition/reverseEngineering.js";
import { registerReverseEngineeringTools } from "./registerReverseEngineeringTools.js";
-import { CutterBridgeClient } from "../cutter/CutterBridgeClient.js";
-import { CutterBridgeService } from "../cutter/CutterBridgeService.js";
import { registerCutterTools } from "./registerCutterTools.js";
-import { GdbSessionManager } from "../gdb/GdbSessionManager.js";
-import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js";
import { registerGdbTools } from "./registerGdbTools.js";
-import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js";
import { registerRizinDebugTools } from "./registerRizinDebugTools.js";
-export interface CreateServerOptions {
+export interface CreateServerOptions extends CapabilityServiceOptions {
readonly analysisSettings?: AnalysisSettings;
/** Preserve the connection's creation time when materializing its runtime. */
readonly startedAt?: string;
@@ -210,191 +108,11 @@ export interface CreateServerOptions {
readonly mcpServer?: EvidenceMcpServer;
readonly environment?: Readonly;
readonly delivery?: ToolResultDelivery;
- readonly packetCapture?: PacketCaptureService;
- readonly signalRecording?: SignalRecordingService;
- readonly wasmArtifact?: WasmArtifactService;
- readonly apkSignature?: ApkSignatureService;
readonly providerEnvironment?: Readonly;
- readonly evmInterface?: EvmInterfaceService;
- readonly sqliteDatabase?: SqliteDatabaseService;
readonly logger?: Logger;
- readonly binaryLayout?: BinaryLayoutService;
- readonly goBinary?: GoBinaryService;
- readonly il2cppMetadata?: Il2cppMetadataService;
- readonly rustBinary?: RustBinaryService;
- readonly pythonPyc?: PythonPycService;
- readonly erlangBeam?: BeamService;
- readonly electronFuses?: ElectronFuseService;
- readonly recordedCrash?: RecordedCrashService;
- readonly firmwareAnalysis?: FirmwareAnalysisPort;
- readonly javascriptRecovery?: JavaScriptRecoveryPort;
- readonly webModuleTrace?: WebModuleTraceService;
- readonly webSourceLocation?: WebSourceLocationService;
- readonly webRuntime?: WebRuntimeService;
- readonly webNetworkCapture?: WebNetworkCaptureService;
- readonly androidAnalysis?: AndroidAnalysisPort;
- readonly apktoolAnalysis?: ApktoolResourceAnalysisPort;
- readonly pcapAnalysis?: PcapAnalysisPort;
- readonly flutterAnalysis?: FlutterBuildAnalysisPort;
- readonly jebAnalysis?: JebAnalysisPort;
- readonly adbDeviceAnalysis?: AdbDeviceAnalysisPort;
- readonly browserObservation?: BrowserObservationPort;
- readonly browserScenarioCapture?: BrowserScenarioCapturePort;
- readonly electronObservation?: ElectronObservationPort;
- /** Selects who closes the shared optional observation providers. */
- readonly optionalObservationProviderOwner?: "server" | "caller";
- readonly electronActiveObservation?: ElectronActiveObservationPort;
- readonly javascriptRuntimeObservation?: JavaScriptRuntimeObservationPort;
- readonly fridaInstrumentation?: FridaInstrumentationPort;
readonly availabilityPolicy?: () => AvailabilityPolicy;
- readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
}
-/** Apply the shared TShark selection rule without starting the tool. */
-const tsharkLocatable = (environment: Readonly): boolean => {
- try {
- return (
- locateWiresharkTool(
- selectWiresharkTool(environment, "tshark"),
- environment,
- ) !== undefined
- );
- } catch {
- // A relative REA_TSHARK_COMMAND is configuration the tool reports at execution.
- return false;
- }
-};
-
-const installSessionToolAvailability = (
- session: BinarySessionPort | undefined,
- options: CreateServerOptions,
- environment: Readonly,
- fridaInstrumentation: FridaInstrumentationPort,
-) => {
- if (session === undefined) return undefined;
- const { platform, arch } = process;
- // Each provider owns its verified host; discovery and execution share it.
- const verifiedHost = {
- packetCapture: tsharkHostSupported(platform),
- pcapCapture: pcapCaptureHostSupported(platform),
- signalRecording: sigrokHostSupported(platform),
- evmInterface: evmHostSupported(platform, arch),
- pwntools: pwntoolsHostSupported(platform, arch),
- firmware: firmwareHostSupported(platform),
- javascriptRecovery: wakaruHostSupported(platform, arch),
- };
- const pwntools =
- verifiedHost.pwntools && isAbsolute(environment.REA_PWNTOOLS_PYTHON ?? "");
- // Injected services own their availability on any host. Otherwise no
- // configuration lets an integration pass its execution host check.
- const outsideVerifiedHost = (injected: unknown, supported: boolean) =>
- injected === undefined && !supported;
- const basePolicy = sessionAvailabilityPolicy(options.availabilityPolicy, {
- optionalProviderLoadFailures: options.optionalProviderLoadFailures,
- hostUnsupported: {
- persistentDebugger: !persistentDebuggerHostSupported(platform),
- pcapCapture: outsideVerifiedHost(
- options.pcapAnalysis,
- verifiedHost.pcapCapture,
- ),
- packetCapture: outsideVerifiedHost(
- options.packetCapture,
- verifiedHost.packetCapture,
- ),
- signalRecording: outsideVerifiedHost(
- options.signalRecording,
- verifiedHost.signalRecording,
- ),
- evmInterface: outsideVerifiedHost(
- options.evmInterface,
- verifiedHost.evmInterface,
- ),
- binaryLayout: outsideVerifiedHost(
- options.binaryLayout,
- verifiedHost.pwntools,
- ),
- recordedCrash: outsideVerifiedHost(
- options.recordedCrash,
- verifiedHost.pwntools,
- ),
- firmwareInspection: outsideVerifiedHost(
- options.firmwareAnalysis,
- verifiedHost.firmware,
- ),
- firmwareExtraction: outsideVerifiedHost(
- options.firmwareAnalysis,
- verifiedHost.firmware,
- ),
- javascriptRecovery: outsideVerifiedHost(
- options.javascriptRecovery,
- verifiedHost.javascriptRecovery,
- ),
- },
- optionalFeatures: {
- packetCaptureEnabled:
- options.packetCapture !== undefined ||
- (verifiedHost.packetCapture && tsharkLocatable(environment)),
- signalRecordingEnabled:
- options.signalRecording !== undefined ||
- (verifiedHost.signalRecording &&
- isAbsolute(environment.REA_SIGROK_COMMAND ?? "")),
- wasmArtifactEnabled:
- options.wasmArtifact !== undefined ||
- isAbsolute(environment.REA_WABT_BIN_DIRECTORY ?? ""),
- evmInterfaceEnabled:
- options.evmInterface !== undefined || verifiedHost.evmInterface,
- webModuleResolutionEnabled:
- options.webModuleTrace !== undefined ||
- isAbsolute(environment.REA_BROWSER_EXECUTABLE ?? ""),
- recordedCrashEnabled: options.recordedCrash !== undefined || pwntools,
- binaryLayoutEnabled: options.binaryLayout !== undefined || pwntools,
- firmwareInspectionEnabled:
- options.firmwareAnalysis !== undefined ||
- (verifiedHost.firmware &&
- isAbsolute(environment.REA_BINWALK_COMMAND ?? "")),
- firmwareExtractionEnabled:
- options.firmwareAnalysis !== undefined ||
- (verifiedHost.firmware &&
- isAbsolute(environment.REA_UNBLOB_COMMAND ?? "")),
- browserObservationEnabled: options.browserObservation !== undefined,
- javascriptRecoveryEnabled:
- options.javascriptRecovery !== undefined ||
- (verifiedHost.javascriptRecovery &&
- isAbsolute(environment.REA_WAKARU_COMMAND ?? "")),
- browserScenarioEnabled: options.browserScenarioCapture !== undefined,
- electronObservationEnabled: options.electronObservation !== undefined,
- electronAutomationEnabled:
- options.electronActiveObservation !== undefined,
- v8InspectorObservationEnabled:
- options.javascriptRuntimeObservation !== undefined,
- },
- });
- const policy = (): AvailabilityPolicy => {
- const value = basePolicy();
- const fridaFailure =
- options.optionalProviderLoadFailures?.fridaInstrumentation ??
- fridaInstrumentation.availabilityFailure?.();
- return fridaFailure === undefined
- ? value
- : {
- ...value,
- optionalProviderLoadFailures: {
- ...value.optionalProviderLoadFailures,
- fridaInstrumentation: fridaFailure,
- },
- };
- };
- return {
- policy,
- ...(options.evmInterface !== undefined || !verifiedHost.evmInterface
- ? {}
- : {
- evmLimiterAvailability: (signal: AbortSignal) =>
- evmLimiterAvailability(environment, { signal }),
- }),
- };
-};
-
const createMcpServer = (
session: BinarySessionPort | undefined,
delivery: ToolResultDelivery,
@@ -456,60 +174,60 @@ export const createServer = (
environment,
);
const delivery = selectToolResultDelivery(environment, options.delivery);
- const evmInterface =
- options.evmInterface ?? evmInterfaceModule.create(environment);
- const sqliteDatabase =
- options.sqliteDatabase ?? createSqliteDatabaseService(environment);
- const binaryLayout =
- options.binaryLayout ?? createBinaryLayoutService(environment);
- const recordedCrash =
- options.recordedCrash ?? createRecordedCrashService(environment);
- const webSourceLocation =
- options.webSourceLocation ?? createWebSourceLocationService(environment);
- const webModuleTrace =
- options.webModuleTrace ?? createWebModuleTraceService(environment);
- const desktopApplication = createDesktopApplicationService();
- const goBinary = options.goBinary ?? createGoBinaryService();
- const rustBinary = options.rustBinary ?? createRustBinaryService();
- const erlangBeam = options.erlangBeam ?? createBeamService();
- const modelArtifact = createModelArtifactService();
- const gpuCodeObjects = createGpuCodeObjectsService();
- const il2cppMetadata =
- options.il2cppMetadata ?? createIl2cppMetadataService();
- const peResources = createPeResourcesService();
- const nativeExports = createNativeExportsService();
- const devicetree = createDevicetreeService();
- const elfNotes = createElfNotesService();
- const appleIps = createAppleIpsService();
- const kernelModule = createKernelModuleService();
- const peLoadConfig = createPeLoadConfigService();
- const peCertificates = createPeCertificatesService();
- const peImports = createPeImportsService();
- const electronFuses = options.electronFuses ?? createElectronFuseService();
- const pythonPyc = options.pythonPyc ?? createPythonPycService();
- const webNetworkCapture =
- options.webNetworkCapture ??
- createWebNetworkCaptureService(environment, analysisSettings.read);
- const javascriptRecovery =
- options.javascriptRecovery ?? createJavaScriptRecoveryProvider(environment);
- const artifactResources = new ArtifactResourceScope();
- const reverseEngineering =
- createReverseEngineeringService(providerEnvironment);
- const firmwareAnalysis = new FirmwareAnalysisService(
- options.firmwareAnalysis ?? createFirmwareAnalysisProvider(environment),
- );
- const selectedOptions = {
- ...options,
+ const capabilities = createCapabilityServices({
environment,
- delivery,
+ providerEnvironment,
+ settings: analysisSettings.read,
+ options,
+ platform: process.platform,
+ arch: process.arch,
+ });
+ const {
evmInterface,
sqliteDatabase,
binaryLayout,
recordedCrash,
webSourceLocation,
webModuleTrace,
+ desktopApplication,
+ goBinary,
+ rustBinary,
+ erlangBeam,
+ modelArtifact,
+ gpuCodeObjects,
+ il2cppMetadata,
+ peResources,
+ nativeExports,
+ devicetree,
+ elfNotes,
+ appleIps,
+ kernelModule,
+ peLoadConfig,
+ peCertificates,
+ peImports,
+ electronFuses,
+ pythonPyc,
+ webNetworkCapture,
javascriptRecovery,
- };
+ artifactResources,
+ reverseEngineering,
+ firmwareAnalysis,
+ gdbSessions,
+ rizinDebugSessions,
+ android,
+ pcap,
+ flutter,
+ fridaInstrumentation,
+ wasmArtifact,
+ apkSignature,
+ packetCapture,
+ signalRecording,
+ webRuntime,
+ apktool,
+ jeb,
+ adbDevice,
+ cutter,
+ } = capabilities.services;
const startedAt = options.startedAt ?? new Date().toISOString();
const logger = options.logger ?? silentLogger;
const server =
@@ -536,122 +254,35 @@ export const createServer = (
),
);
}
- const gdbSessions = new GdbSessionManager({
- environment: providerEnvironment,
- });
- const rizinDebugSessions = new RizinDebugSessionManager(providerEnvironment);
- const android =
- options.androidAnalysis ??
- createAndroidAnalysisProvider(
- environment,
- undefined,
- analysisSettings.read,
- );
- const pcap = options.pcapAnalysis ?? createPcapAnalysisProvider(environment);
- const flutter =
- options.flutterAnalysis ?? createFlutterBuildAnalysisProvider();
- const fridaInstrumentation =
- options.fridaInstrumentation ?? new FridaInstrumentationManager();
- const wasmArtifact = wasmArtifactModule.create(
- environment,
- options.wasmArtifact,
- analysisSettings.read,
- );
- const apkSignature = apkSignatureModule.create(
- environment,
- options.apkSignature,
- );
- const packetCapture =
- options.packetCapture ?? createPacketCaptureService(environment);
- const signalRecording =
- options.signalRecording ?? createSignalRecordingService(environment);
- const webRuntime = options.webRuntime ?? createWebRuntimeService();
- const apktool =
- options.apktoolAnalysis ??
- createApktoolResourceAnalysisProvider(environment);
- const jeb = options.jebAnalysis ?? createJebAnalysisProvider(environment);
- const adbDevice =
- options.adbDeviceAnalysis ?? createAdbDeviceAnalysisProvider(environment);
- const cutter = new CutterBridgeService(
- new CutterBridgeClient(providerEnvironment),
- );
- let providerClose: Promise | undefined;
- const closeProviders = (): Promise => {
- providerClose ??= Promise.allSettled([
- android.close(),
- cutter.close(),
- fridaInstrumentation.closeAll(),
- wasmArtifact.close(),
- apkSignature.close(),
- packetCapture.close(),
- signalRecording.close(),
- apktool.close(),
- jeb.close(),
- adbDevice.close(),
- flutter.close(),
- pcap.close(),
- evmInterface.close(),
- sqliteDatabase.close(),
- binaryLayout.close(),
- recordedCrash.close(),
- webSourceLocation.close(),
- webModuleTrace.close(),
- desktopApplication.close(),
- goBinary.close(),
- rustBinary.close(),
- erlangBeam.close(),
- il2cppMetadata.close(),
- modelArtifact.close(),
- gpuCodeObjects.close(),
- peResources.close(),
- nativeExports.close(),
- devicetree.close(),
- elfNotes.close(),
- appleIps.close(),
- kernelModule.close(),
- peLoadConfig.close(),
- peCertificates.close(),
- peImports.close(),
- electronFuses.close(),
- pythonPyc.close(),
- webNetworkCapture.close(),
- javascriptRecovery.close?.() ?? Promise.resolve(),
- ...(options.optionalObservationProviderOwner === "caller"
- ? []
- : [
- options.browserObservation?.close?.() ?? Promise.resolve(),
- options.browserScenarioCapture?.close?.() ?? Promise.resolve(),
- options.electronObservation?.close?.() ?? Promise.resolve(),
- options.electronActiveObservation?.close?.() ?? Promise.resolve(),
- options.javascriptRuntimeObservation?.close?.() ??
- Promise.resolve(),
- ]),
- webRuntime.provider.close?.() ?? Promise.resolve(),
- artifactResources.close(),
- reverseEngineering.close(),
- firmwareAnalysis.close(),
- gdbSessions.closeAll(),
- rizinDebugSessions.closeAll(),
- ])
- .then((results) => {
- const failures = results.flatMap((result) =>
- result.status === "rejected" ? [result.reason] : [],
- );
- if (failures.length > 0)
- throw new AggregateError(failures, "REA provider cleanup failed");
- })
- .catch((cause: unknown) => {
- providerClose = undefined;
- throw cause;
- });
- return providerClose;
- };
- const availability = installSessionToolAvailability(
- session,
- options,
- environment,
- fridaInstrumentation,
- );
+ const closeProviders = capabilities.close;
+ const availability =
+ session === undefined
+ ? undefined
+ : (() => {
+ const basePolicy = sessionAvailabilityPolicy(
+ options.availabilityPolicy,
+ {
+ ...capabilities.availability,
+ optionalProviderLoadFailures:
+ options.optionalProviderLoadFailures,
+ },
+ );
+ return {
+ policy: (): AvailabilityPolicy => {
+ const value = basePolicy();
+ const failures = capabilities.observeProviderLoadFailures();
+ return Object.keys(failures).length === 0
+ ? value
+ : {
+ ...value,
+ optionalProviderLoadFailures: {
+ ...value.optionalProviderLoadFailures,
+ ...failures,
+ },
+ };
+ },
+ };
+ })();
const toolLogger = logger.child({ layer: "server" });
const {
evidenceById,
@@ -782,7 +413,7 @@ export const createServer = (
);
registerBinaryDiagnosticsTools(
server,
- selectedOptions.binaryLayout ?? createBinaryLayoutService(environment),
+ binaryLayout,
createByteEntropyService(artifactResources),
toolLogger,
recordEvidence,
@@ -856,8 +487,7 @@ export const createServer = (
);
registerRecordedCrashTools(
server,
- selectedOptions.recordedCrash ??
- createRecordedCrashService(environment),
+ recordedCrash,
toolLogger,
recordEvidence,
analysisAdmission,
@@ -955,8 +585,7 @@ export const createServer = (
);
registerSqliteTools(
server,
- selectedOptions.sqliteDatabase ??
- createSqliteDatabaseService(environment),
+ sqliteDatabase,
toolLogger,
recordEvidence,
analysisAdmission,
@@ -1059,16 +688,15 @@ export const createServer = (
registerBrowserTools(server, {
...common,
evidenceById,
- browser: selectedOptions.browserObservation,
- loadFailure:
- selectedOptions.optionalProviderLoadFailures?.browserObservation,
+ browser: options.browserObservation,
+ loadFailure: options.optionalProviderLoadFailures?.browserObservation,
});
registerBrowserScenarioTool(server, {
...common,
evidenceById,
- provider: selectedOptions.browserScenarioCapture,
+ provider: options.browserScenarioCapture,
loadFailure:
- selectedOptions.optionalProviderLoadFailures?.browserScenarioCapture,
+ options.optionalProviderLoadFailures?.browserScenarioCapture,
});
registerWebRuntimeTools(
server,
@@ -1092,22 +720,20 @@ export const createServer = (
artifactResources,
environment,
evidenceById,
- electron: selectedOptions.electronObservation,
- electronActive: selectedOptions.electronActiveObservation,
+ electron: options.electronObservation,
+ electronActive: options.electronActiveObservation,
electronFuses,
observationLoadFailure:
- selectedOptions.optionalProviderLoadFailures?.electronObservation,
+ options.optionalProviderLoadFailures?.electronObservation,
activeLoadFailure:
- selectedOptions.optionalProviderLoadFailures
- ?.electronActiveObservation,
+ options.optionalProviderLoadFailures?.electronActiveObservation,
}),
"javascript-runtime": () =>
registerJavaScriptRuntimeObservationTools(server, {
...common,
- runtime: selectedOptions.javascriptRuntimeObservation,
+ runtime: options.javascriptRuntimeObservation,
loadFailure:
- selectedOptions.optionalProviderLoadFailures
- ?.javascriptRuntimeObservation,
+ options.optionalProviderLoadFailures?.javascriptRuntimeObservation,
}),
application: () => {
registerAnalysisSettingsTools(server, analysisSettings);
@@ -1122,26 +748,21 @@ export const createServer = (
registerWebScriptTool(server, { ...common, artifactResources });
registerWebModuleTool(
server,
- selectedOptions.webModuleTrace ??
- createWebModuleTraceService(environment),
+ webModuleTrace,
toolLogger,
recordEvidence,
analysisAdmission,
);
registerWebSourceLocationTool(
server,
- selectedOptions.webSourceLocation ??
- createWebSourceLocationService(environment),
+ webSourceLocation,
toolLogger,
recordEvidence,
analysisAdmission,
);
registerJavaScriptRecoveryTool(
server,
- new JavaScriptRecoveryService(
- selectedOptions.javascriptRecovery ??
- createJavaScriptRecoveryProvider(environment),
- ),
+ new JavaScriptRecoveryService(javascriptRecovery),
toolLogger,
recordEvidence,
analysisAdmission,
@@ -1158,21 +779,12 @@ export const createServer = (
session: () => {
if (session === undefined) return;
registerSessionTools(server, session, toolLogger, {
- ...selectedOptions,
+ environment,
artifactResources,
...(availability === undefined
? {}
: { availabilityPolicy: availability.policy }),
- ...(availability?.evmLimiterAvailability === undefined
- ? {}
- : { evmLimiterAvailability: availability.evmLimiterAvailability }),
- androidAnalysisAvailability: (signal) =>
- android.inspectAvailability(signal),
- adbAnalysisAvailability: (signal) =>
- adbDevice.inspectAvailability(signal),
- apktoolAnalysisAvailability: (signal) =>
- apktool.inspectAvailability(signal),
- jebAnalysisAvailability: (signal) => jeb.inspectAvailability(signal),
+ ...capabilities.probes,
startedAt,
});
},
diff --git a/src/server/registerSessionStatusTool.ts b/src/server/registerSessionStatusTool.ts
index 4de670527b..663b9fdd8b 100644
--- a/src/server/registerSessionStatusTool.ts
+++ b/src/server/registerSessionStatusTool.ts
@@ -1,3 +1,4 @@
+import type { CapabilityAvailabilityProbes } from "../application/CapabilityAvailability.js";
import {
BINARY_SESSION_SUMMARY_OMITTED_FIELDS,
type BinarySessionDetail,
@@ -27,27 +28,8 @@ import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { err } from "../domain/result.js";
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
-/** Provider-owned live probes used by session discovery. */
-export interface SessionAvailabilityProbes {
- readonly androidAnalysisAvailability: (
- signal: AbortSignal,
- ) => Promise;
- readonly adbAnalysisAvailability?: (
- signal: AbortSignal,
- ) => Promise;
- readonly apktoolAnalysisAvailability?: (
- signal: AbortSignal,
- ) => Promise;
- readonly jebAnalysisAvailability?: (
- signal: AbortSignal,
- ) => Promise;
- readonly evmLimiterAvailability?: (
- signal: AbortSignal,
- ) => Promise;
-}
-
/** Inputs required to register the binary session status tool. */
-export interface SessionStatusToolOptions extends SessionAvailabilityProbes {
+export interface SessionStatusToolOptions extends CapabilityAvailabilityProbes {
readonly server: EvidenceMcpServer;
readonly session: BinarySessionPort;
readonly contract: ReturnType>;
@@ -61,7 +43,7 @@ export interface SessionStatusToolOptions extends SessionAvailabilityProbes {
* cannot appear in the response.
*/
const probeSessionAvailability = (
- probes: SessionAvailabilityProbes,
+ probes: CapabilityAvailabilityProbes,
signal: AbortSignal,
advertisesFamily: (family: ToolFamilyId) => boolean,
): Promise<{
diff --git a/src/server/registerSessionTools.ts b/src/server/registerSessionTools.ts
index fffed1dedb..8808ceadc9 100644
--- a/src/server/registerSessionTools.ts
+++ b/src/server/registerSessionTools.ts
@@ -1,3 +1,4 @@
+import type { CapabilityAvailabilityProbes } from "../application/CapabilityAvailability.js";
import { parseEspFlashAddress } from "../domain/firmware/espFlashSelection.js";
import { registerNativeFunctionMatchingTool } from "./registerNativeFunctionMatchingTool.js";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
@@ -34,10 +35,7 @@ import {
registerEvidenceTools,
registerUnknownTools,
} from "./registerSessionRecordTools.js";
-import {
- registerSessionStatusTool,
- type SessionAvailabilityProbes,
-} from "./registerSessionStatusTool.js";
+import { registerSessionStatusTool } from "./registerSessionStatusTool.js";
import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js";
import type { AvailabilityPolicy } from "../application/CapabilityInventory.js";
import { logToolExecution } from "./toolLogging.js";
@@ -150,7 +148,7 @@ const registerProcessTools = ({
);
};
-export interface LifecycleToolRegistration extends SessionAvailabilityProbes {
+export interface LifecycleToolRegistration extends CapabilityAvailabilityProbes {
readonly server: EvidenceMcpServer;
readonly session: BinarySessionPort;
readonly logger: Logger;
@@ -245,7 +243,7 @@ const registerOpenLifecycleTool = ({
};
/** Register MCP-only target lifecycle operations on a long-lived session. */
-export interface SessionToolOptions extends Partial {
+export interface SessionToolOptions extends Partial {
readonly artifactResources: ArtifactResourceScope;
/** Server environment snapshot that process capture resolves and inherits. */
readonly environment: Readonly;
diff --git a/src/server/sessionAvailabilityPolicy.ts b/src/server/sessionAvailabilityPolicy.ts
index 47867f78f9..6883639778 100644
--- a/src/server/sessionAvailabilityPolicy.ts
+++ b/src/server/sessionAvailabilityPolicy.ts
@@ -3,33 +3,8 @@ import {
type AvailabilityPolicy,
type HostRestrictedFeature,
} from "../application/CapabilityInventory.js";
-import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js";
import { platform } from "node:process";
-
-export interface SessionAvailabilityDefaults {
- readonly optionalProviderLoadFailures?:
- | OptionalProviderLoadFailures
- | undefined;
- readonly hostUnsupported?: AvailabilityPolicy["hostUnsupported"];
- readonly optionalFeatures?: Pick<
- AvailabilityPolicy,
- | "packetCaptureEnabled"
- | "signalRecordingEnabled"
- | "wasmArtifactEnabled"
- | "evmInterfaceEnabled"
- | "browserObservationEnabled"
- | "browserScenarioEnabled"
- | "electronObservationEnabled"
- | "electronAutomationEnabled"
- | "v8InspectorObservationEnabled"
- | "javascriptRecoveryEnabled"
- | "webModuleResolutionEnabled"
- | "binaryLayoutEnabled"
- | "recordedCrashEnabled"
- | "firmwareInspectionEnabled"
- | "firmwareExtractionEnabled"
- >;
-}
+import type { CapabilityAvailabilityFacts } from "../application/CapabilityAvailability.js";
/** Either the caller or the observed host can mark an integration unsupported. */
const combineHostUnsupported = (
@@ -46,7 +21,7 @@ const combineHostUnsupported = (
/** Select configured availability reporting or the target-free defaults. */
export const sessionAvailabilityPolicy = (
configured: (() => AvailabilityPolicy) | undefined,
- defaults: SessionAvailabilityDefaults,
+ defaults: CapabilityAvailabilityFacts,
): (() => AvailabilityPolicy) => {
const policy =
configured ??
diff --git a/src/signals/SigrokProviderIdentity.ts b/src/signals/SigrokProviderIdentity.ts
new file mode 100644
index 0000000000..9f7a6cfa48
--- /dev/null
+++ b/src/signals/SigrokProviderIdentity.ts
@@ -0,0 +1,6 @@
+/** Source-owned static provider identity and public operation names. */
+export const SIGROK_PROVIDER_IDENTITY = {
+ id: "sigrok-recording",
+ name: "REA sigrok recording adapter",
+ version: "sigrok-cli@0.7.2",
+} as const;
diff --git a/src/signals/SigrokRecordingProvider.ts b/src/signals/SigrokRecordingProvider.ts
index 1d588f0c7f..aac0f0237c 100644
--- a/src/signals/SigrokRecordingProvider.ts
+++ b/src/signals/SigrokRecordingProvider.ts
@@ -1,3 +1,4 @@
+import { SIGROK_PROVIDER_IDENTITY } from "./SigrokProviderIdentity.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { artifactFileCancellationCause } from "../artifacts/ArtifactFileOperation.js";
import {
@@ -68,11 +69,7 @@ import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
import { privateWorkspaceFailure } from "../process/privateWorkspaceFailure.js";
const OPERATION = "decode_signal_recording";
-export const SIGROK_PROVIDER_IDENTITY = {
- id: "sigrok-recording",
- name: "REA sigrok recording adapter",
- version: "sigrok-cli@0.7.2",
-} as const;
+
type Launcher = NonNullable[2]>["launcher"];
/** Signal decoding runs only on Linux; discovery and execution share this check. */
diff --git a/src/sqlite/SqliteDatabaseFailures.ts b/src/sqlite/SqliteDatabaseFailures.ts
index d9dd7b8b35..e049b61318 100644
--- a/src/sqlite/SqliteDatabaseFailures.ts
+++ b/src/sqlite/SqliteDatabaseFailures.ts
@@ -30,10 +30,10 @@ import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
import {
- SQLITE_PROVIDER_IDENTITY,
DEFAULT_SQLITE_WORKER_TIMEOUT_MS,
MAX_SQLITE_WORKER_TIMEOUT_MS,
} from "../domain/sqlite/sqliteDatabase.js";
+import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/SqliteProviderIdentity.js";
import { SQLITE_DATABASE_LIMITS } from "./SqliteDatabaseLimits.js";
import { SqliteSnapshotAcquisitionFailure } from "./SqliteDatabaseSnapshot.js";
diff --git a/src/sqlite/SqliteDatabaseProvider.ts b/src/sqlite/SqliteDatabaseProvider.ts
index 43a14636a7..f003f0a375 100644
--- a/src/sqlite/SqliteDatabaseProvider.ts
+++ b/src/sqlite/SqliteDatabaseProvider.ts
@@ -25,10 +25,10 @@ import {
inspectSqliteDatabaseInputSchema,
sqliteDatabaseSchema,
type InspectSqliteDatabaseInput,
- SQLITE_PROVIDER_IDENTITY,
DEFAULT_SQLITE_WORKER_TIMEOUT_MS,
sqliteInspectionParameters,
} from "../domain/sqlite/sqliteDatabase.js";
+import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/SqliteProviderIdentity.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
import {
diff --git a/src/sqlite/SqliteDatabaseSnapshot.ts b/src/sqlite/SqliteDatabaseSnapshot.ts
index e11ab0d7d1..79daeec560 100644
--- a/src/sqlite/SqliteDatabaseSnapshot.ts
+++ b/src/sqlite/SqliteDatabaseSnapshot.ts
@@ -8,7 +8,7 @@ import {
AnalysisInputError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
-import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/sqliteDatabase.js";
+import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/SqliteProviderIdentity.js";
import {
withArtifactFileHandle,
assertArtifactReadActive,
diff --git a/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts b/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts
index 99950d4545..1f2156ce19 100644
--- a/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts
+++ b/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts
@@ -7,7 +7,7 @@ import { join } from "node:path";
import { z } from "zod";
import { expect, it, onTestFinished } from "vitest";
import { PwntoolsLayoutProvider } from "../../../src/native/pwntools/PwntoolsLayoutProvider.js";
-import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsProviderIdentity.js";
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js";
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
diff --git a/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts b/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts
index f2d3d05db6..b39b3a6eed 100644
--- a/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts
+++ b/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts
@@ -10,7 +10,7 @@ import {
removeTestWorkspace,
} from "../../support/workspace/workspaceFixture.js";
import { PwntoolsRecordedCrashProvider } from "../../../src/native/pwntools/PwntoolsRecordedCrashProvider.js";
-import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsProviderIdentity.js";
import {
spawnOwnedProviderProcess,
type OwnedProviderProcessSpawnOptions,
diff --git a/tests/boundary/browser/historicalCaptureDecoder.test.ts b/tests/boundary/browser/historicalCaptureDecoder.test.ts
index bdb757c4c5..dfb62de77a 100644
--- a/tests/boundary/browser/historicalCaptureDecoder.test.ts
+++ b/tests/boundary/browser/historicalCaptureDecoder.test.ts
@@ -22,7 +22,7 @@ import { MitmproxyCaptureAdapter } from "../../../src/browser/history/MitmproxyC
import {
MITMPROXY_CAPTURE_PROVIDER_IDENTITY,
HAR_CAPTURE_PROVIDER_IDENTITY,
-} from "../../../src/browser/history/CaptureRelease.js";
+} from "../../../src/browser/history/CaptureProviderIdentities.js";
import {
inspectWebNetworkCaptureInputSchema,
WEB_NETWORK_CAPTURE_LIMITS,
diff --git a/tests/boundary/filesystem/directAnalysisPermissions.test.ts b/tests/boundary/filesystem/directAnalysisPermissions.test.ts
index 34effe4d11..64344b9457 100644
--- a/tests/boundary/filesystem/directAnalysisPermissions.test.ts
+++ b/tests/boundary/filesystem/directAnalysisPermissions.test.ts
@@ -20,7 +20,7 @@ import { createEvidence } from "../../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { REA_WORKFLOW_PROVIDER } from "../../../src/domain/investigationProviders.js";
import { workflowAnalysisProfile } from "../../../src/application/WorkflowAnalysisProfile.js";
-import { HOPPER_PROVIDER_IDENTITY } from "../../../src/hopper/HopperProviderCapabilities.js";
+import { HOPPER_PROVIDER_IDENTITY } from "../../../src/hopper/HopperProviderMetadata.js";
import { resolveHopperAnalysisProfile } from "../../../src/hopper/HopperAnalysisProfile.js";
diff --git a/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts b/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts
index 26b1ff06d4..5c418312ec 100644
--- a/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts
+++ b/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts
@@ -47,10 +47,8 @@ import { isProfilelessNativeDispatchSnapshot } from "../../../src/application/bi
import { OwnedStagedOutput } from "../../../src/filesystem/OwnedStagedOutput.js";
import { ProviderAdapterError } from "../../../src/domain/providerAdapterError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
-import {
- CAPABILITIES as GHIDRA_CAPABILITIES,
- GHIDRA_PROVIDER_IDENTITY,
-} from "../../../src/ghidra/GhidraProviderCapabilities.js";
+import { CAPABILITIES as GHIDRA_CAPABILITIES } from "../../../src/ghidra/GhidraProviderCapabilities.js";
+import { GHIDRA_PROVIDER_IDENTITY } from "../../../src/ghidra/GhidraProviderMetadata.js";
import { nativeInventoryRowsSchemas } from "../../../src/domain/native/nativeInventory.js";
import { APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE } from "../../../src/native/NativeMacOSProviderMetadata.js";
diff --git a/tests/boundary/filesystem/productCatalog.test.ts b/tests/boundary/filesystem/productCatalog.test.ts
index a3aad84c34..7a2ce1256f 100644
--- a/tests/boundary/filesystem/productCatalog.test.ts
+++ b/tests/boundary/filesystem/productCatalog.test.ts
@@ -10,20 +10,20 @@ import { BYTE_ENTROPY_PROVIDER } from "../../../src/domain/investigationProvider
import { SUPPORTED_CLIENT_DEFINITIONS } from "../../../src/application/SupportedClients.js";
import { CLI_COMMAND_NAMES } from "../../../src/cliCommandNames.js";
import { createCli } from "../../../src/cli.js";
-import { FRIDA_PROVIDER_IDENTITY } from "../../../src/frida/FridaInstrumentationManager.js";
+import { FRIDA_PROVIDER_IDENTITY } from "../../../src/frida/FridaProviderIdentity.js";
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import {
HOPPER_OPERATIONS,
HOPPER_PROVIDER_IDENTITY,
-} from "../../../src/hopper/HopperProviderCapabilities.js";
+} from "../../../src/hopper/HopperProviderMetadata.js";
import {
GHIDRA_PROVIDER_IDENTITY,
GHIDRA_OPERATIONS,
-} from "../../../src/ghidra/GhidraProviderCapabilities.js";
-import { OBJDUMP_PROVIDER_IDENTITY } from "../../../src/objdump/ObjdumpCommand.js";
-import { RIZIN_PROVIDER_IDENTITY } from "../../../src/rizin/RizinCommand.js";
-import { GDB_PROVIDER_IDENTITY } from "../../../src/gdb/GdbSessionManager.js";
-import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js";
+} from "../../../src/ghidra/GhidraProviderMetadata.js";
+import { OBJDUMP_PROVIDER_IDENTITY } from "../../../src/objdump/ObjdumpProviderIdentity.js";
+import { RIZIN_PROVIDER_IDENTITY } from "../../../src/rizin/RizinProviderIdentity.js";
+import { GDB_PROVIDER_IDENTITY } from "../../../src/gdb/GdbProviderIdentity.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsProviderIdentity.js";
import {
documentationFactIssues,
skillReferenceIssues,
diff --git a/tests/boundary/mcp/createServerCleanup.test.ts b/tests/boundary/mcp/createServerCleanup.test.ts
index 909b0fcd67..4ac315297f 100644
--- a/tests/boundary/mcp/createServerCleanup.test.ts
+++ b/tests/boundary/mcp/createServerCleanup.test.ts
@@ -6,6 +6,8 @@ import { InvestigationRecords } from "../../../src/application/investigation/Inv
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
import { createServer } from "../../../src/server/createServer.js";
+import { createBrowserObservationProvider } from "../../../src/composition/browserObservation.js";
+import { createEvmInterfaceService } from "../../../src/composition/evm.js";
afterEach(() => vi.restoreAllMocks());
@@ -47,3 +49,46 @@ it("preserves both shutdown failures and retries provider cleanup after onclose"
transportClose.mockRestore();
await session.close();
});
+
+it("retries only the failed injected capability after its peer has closed", async () => {
+ // Compose the real typed owners without starting browser observation or EVM
+ // analysis. Only their close outcomes are controlled by this regression.
+ const browser = createBrowserObservationProvider();
+ const evm = createEvmInterfaceService({});
+ const failure = new Error("Controlled browser shutdown refusal");
+ const browserClose = vi
+ .spyOn(browser, "close")
+ .mockRejectedValueOnce(failure)
+ .mockResolvedValue(undefined);
+ const evmClose = vi.spyOn(evm, "close").mockResolvedValue(undefined);
+ const server = createServer(
+ {
+ kind: "fixed",
+ analysis: {
+ execute: async () => {
+ throw new Error("Cleanup must not execute analysis");
+ },
+ },
+ },
+ { environment: {}, browserObservation: browser, evmInterface: evm },
+ );
+ try {
+ const first: unknown = await server
+ .close()
+ .catch((cause: unknown) => cause);
+ const causes = (cause: unknown): unknown[] =>
+ cause instanceof AggregateError ? cause.errors.flatMap(causes) : [cause];
+ expect(causes(first)).toEqual([failure]);
+ expect(browserClose).toHaveBeenCalledTimes(1);
+ expect(evmClose).toHaveBeenCalledTimes(1);
+ await server.close();
+ expect(browserClose).toHaveBeenCalledTimes(2);
+ expect(evmClose).toHaveBeenCalledTimes(1);
+ await server.close();
+ expect(browserClose).toHaveBeenCalledTimes(2);
+ expect(evmClose).toHaveBeenCalledTimes(1);
+ } finally {
+ browserClose.mockResolvedValue(undefined);
+ await server.close();
+ }
+});
diff --git a/tests/boundary/mcp/rawBinaryMcp.test.ts b/tests/boundary/mcp/rawBinaryMcp.test.ts
index d2c2326844..497f542898 100644
--- a/tests/boundary/mcp/rawBinaryMcp.test.ts
+++ b/tests/boundary/mcp/rawBinaryMcp.test.ts
@@ -19,7 +19,7 @@ import {
} from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { silentLogger } from "../../../src/logger.js";
-import { GHIDRA_PROVIDER_IDENTITY } from "../../../src/ghidra/GhidraProviderCapabilities.js";
+import { GHIDRA_PROVIDER_IDENTITY } from "../../../src/ghidra/GhidraProviderMetadata.js";
import { RAW_ONLY_ARCHITECTURES } from "../../../src/domain/binaryTargetTypes.js";
import type { CallToolResult } from "@modelcontextprotocol/server";
diff --git a/tests/boundary/mcp/serverLifecycleMcp.test.ts b/tests/boundary/mcp/serverLifecycleMcp.test.ts
index 2bcdd86eb8..051cd1ea50 100644
--- a/tests/boundary/mcp/serverLifecycleMcp.test.ts
+++ b/tests/boundary/mcp/serverLifecycleMcp.test.ts
@@ -494,7 +494,7 @@ it("closes the Inspector owner with peers and retries its retained cleanup", asy
await server.close();
expect(inspectorCloseAttempts).toBe(2);
- expect(peerCloseAttempts).toBe(2);
+ expect(peerCloseAttempts).toBe(1);
});
it("does not close a caller-owned Inspector observation provider", async () => {
diff --git a/tests/boundary/mcp/transportCleanup.test.ts b/tests/boundary/mcp/transportCleanup.test.ts
index 0b7247b2a2..2e2a485597 100644
--- a/tests/boundary/mcp/transportCleanup.test.ts
+++ b/tests/boundary/mcp/transportCleanup.test.ts
@@ -1,10 +1,37 @@
import { expect, it, vi } from "vitest";
+import { JadxProvider } from "../../../src/android/JadxProvider.js";
import { createManagedBinarySession } from "../../../src/composition/binary.js";
import { silentLogger } from "../../../src/logger.js";
import { startMcpTransport } from "../../../src/main/transport.js";
import { createServer } from "../../../src/server/createServer.js";
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
+const startOneServer = (
+ session: Parameters[1],
+ construct: typeof createServer,
+) =>
+ startMcpTransport(
+ {
+ env: {},
+ serve: async (factory) => {
+ await factory({ era: "modern" });
+ return { close: async () => undefined };
+ },
+ createServer: construct,
+ writeStderr: () => undefined,
+ setExitCode: () => undefined,
+ registerShutdown: () => () => undefined,
+ },
+ session,
+ {
+ logger: silentLogger,
+ serverLogger: silentLogger,
+ environment: {},
+ delivery: createToolResultDelivery(undefined),
+ loadOptionalProviders: async () => ({}),
+ },
+ );
+
it("preserves Android sibling cleanup failures and retries only unfinished owners", async () => {
const session = createManagedBinarySession();
const failures = [
@@ -72,3 +99,71 @@ it("preserves Android sibling cleanup failures and retries only unfinished owner
await session.close();
}
});
+
+it("releases the Android fallback when its actual server owner closes successfully", async () => {
+ const session = createManagedBinarySession();
+ const underlyingClose = vi.spyOn(JadxProvider.prototype, "close");
+ let server: ReturnType | undefined;
+ const transport = await startOneServer(session, (source, options) => {
+ server = createServer(source, options);
+ return server;
+ });
+ try {
+ expect(transport.ok).toBe(true);
+ if (server === undefined)
+ throw new Error("Expected a constructed SDK server");
+ await server.close();
+ expect(underlyingClose).toHaveBeenCalledTimes(1);
+ await transport.closeAndroid();
+ await transport.closeAndroid();
+ expect(underlyingClose).toHaveBeenCalledTimes(1);
+ } finally {
+ await transport.handle.close();
+ await transport.closeAndroid();
+ await transport.closeOptionalProviders();
+ await session.close();
+ underlyingClose.mockRestore();
+ }
+});
+
+it.each(["server close", "server construction"] as const)(
+ "keeps the Android fallback after failed %s until the exact provider closes",
+ async (stage) => {
+ const session = createManagedBinarySession();
+ const failure = new Error("Controlled first Android cleanup refusal");
+ const underlyingClose = vi
+ .spyOn(JadxProvider.prototype, "close")
+ .mockRejectedValueOnce(failure);
+ let server: ReturnType | undefined;
+ const transport = await startOneServer(session, (source, options) => {
+ if (stage === "server construction")
+ throw new Error("Controlled server construction failure");
+ server = createServer(source, options);
+ return server;
+ });
+ try {
+ expect(transport.ok).toBe(stage === "server close");
+ if (stage === "server close") {
+ if (server === undefined)
+ throw new Error("Expected a constructed SDK server");
+ await expect(server.close()).rejects.toMatchObject({
+ name: "AggregateError",
+ errors: [{ name: "AggregateError", errors: [failure] }],
+ });
+ }
+ // Failed construction also attempts cleanup before returning its partial
+ // owner. Both paths must preserve the same failed provider for fallback.
+ expect(underlyingClose).toHaveBeenCalledTimes(1);
+ await transport.closeAndroid();
+ expect(underlyingClose).toHaveBeenCalledTimes(2);
+ await transport.closeAndroid();
+ expect(underlyingClose).toHaveBeenCalledTimes(2);
+ } finally {
+ await transport.handle.close();
+ await transport.closeAndroid();
+ await transport.closeOptionalProviders();
+ await session.close();
+ underlyingClose.mockRestore();
+ }
+ },
+);
diff --git a/tests/boundary/native/pwntoolsArtifactCleanup.test.ts b/tests/boundary/native/pwntoolsArtifactCleanup.test.ts
index c0f3cefb83..359b42ec67 100644
--- a/tests/boundary/native/pwntoolsArtifactCleanup.test.ts
+++ b/tests/boundary/native/pwntoolsArtifactCleanup.test.ts
@@ -55,7 +55,7 @@ import { prepareProcessOwnershipInspection } from "../../../src/process/ProcessO
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import { binaryLayoutFixture } from "../../fixtures/binaryDiagnostics/layout.js";
import { PwntoolsDecoder } from "../../../src/native/pwntools/PwntoolsDecoder.js";
-import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsProviderIdentity.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
const makeDecoder = () =>
diff --git a/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts b/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts
index 8234f25930..67fca95eba 100644
--- a/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts
+++ b/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts
@@ -11,7 +11,7 @@ import {
hopperLoaderArgsForTarget,
resolveHopperAnalysisProfile,
} from "../../../../src/hopper/HopperAnalysisProfile.js";
-import { HOPPER_PROVIDER_IDENTITY } from "../../../../src/hopper/HopperProviderCapabilities.js";
+import { HOPPER_PROVIDER_IDENTITY } from "../../../../src/hopper/HopperProviderMetadata.js";
let inspectionResources = createInspectionResources();
let directory: string | undefined;
diff --git a/tests/boundary/providers/hopper/hopperClient.protocol.test.ts b/tests/boundary/providers/hopper/hopperClient.protocol.test.ts
index 02a6ae73b6..26196a19ef 100644
--- a/tests/boundary/providers/hopper/hopperClient.protocol.test.ts
+++ b/tests/boundary/providers/hopper/hopperClient.protocol.test.ts
@@ -3,7 +3,7 @@ import { once } from "node:events";
import { describe, expect, it, onTestFinished, vi } from "vitest";
import { HopperProcessError } from "../../../../src/domain/hopperErrors.js";
-import { HOPPER_OPERATIONS } from "../../../../src/hopper/HopperProviderCapabilities.js";
+import { HOPPER_OPERATIONS } from "../../../../src/hopper/HopperProviderMetadata.js";
import { HopperClient } from "../../../../src/hopper/HopperClient.js";
import type { HopperDiagnostic } from "../../../../src/hopper/HopperDiagnostics.js";
import { providerCleanupFailure } from "../../../../src/hopper/HopperDiagnostics.js";
diff --git a/tests/composition/idaProvider.test.ts b/tests/composition/idaProvider.test.ts
index f12981cafc..2c6c08f934 100644
--- a/tests/composition/idaProvider.test.ts
+++ b/tests/composition/idaProvider.test.ts
@@ -15,7 +15,7 @@ import { createEvidence } from "../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js";
import { createAnalysisExecution } from "../../src/application/AnalysisProvider.js";
import { EnhancedTools } from "../../src/application/EnhancedTools.js";
-import { IDA_PROVIDER_IDENTITY } from "../../src/ida/IdaProviderCapabilities.js";
+import { IDA_PROVIDER_IDENTITY } from "../../src/ida/IdaProviderMetadata.js";
import { parseBinaryTarget } from "../../src/application/BinaryTargetResolver.js";
import { fat } from "../../src/domain/binaryTarget.fixture.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
diff --git a/tests/composition/liveToolFactories.test.ts b/tests/composition/liveToolFactories.test.ts
new file mode 100644
index 0000000000..be319d81f8
--- /dev/null
+++ b/tests/composition/liveToolFactories.test.ts
@@ -0,0 +1,103 @@
+import { readdir } from "node:fs/promises";
+
+import { expect, it, vi } from "vitest";
+
+import { createCutterBridgeService } from "../../src/composition/cutter.js";
+import {
+ createGdbSessionManager,
+ createRizinDebugSessionManager,
+} from "../../src/composition/debuggerSessions.js";
+import { createFridaInstrumentationProvider } from "../../src/composition/frida.js";
+import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
+
+it("keeps Frida binding acquisition lazy and one owner's shutdown local", async () => {
+ // Only the binding boundary is simulated; neither native Frida nor a target
+ // is acquired in this factory/lifetime test.
+ const load = vi.fn(async () => {
+ throw new Error("Authored fixture has no native Frida binding");
+ });
+ const first = createFridaInstrumentationProvider({
+ deviceManagerLoader: load,
+ });
+ const second = createFridaInstrumentationProvider({
+ deviceManagerLoader: load,
+ });
+ try {
+ expect(load).not.toHaveBeenCalled();
+ await first.closeAll();
+ expect(load).not.toHaveBeenCalled();
+ expect(await first.listDevices()).toMatchObject({
+ ok: false,
+ error: { reason: expect.stringContaining("shutting down") },
+ });
+ expect(load).not.toHaveBeenCalled();
+ expect((await second.listDevices()).ok).toBe(false);
+ expect(load).toHaveBeenCalledTimes(1);
+ } finally {
+ await Promise.all([first.closeAll(), second.closeAll()]);
+ }
+});
+
+it("gives Cutter compositions independent descriptor ownership without startup writes", async () => {
+ const directory = await createTestTempDirectory("rea-cutter-factory-");
+ const environment = { REA_CUTTER_BRIDGE_DIR: directory };
+ const first = createCutterBridgeService(environment, { platform: "linux" });
+ const second = createCutterBridgeService(environment, { platform: "linux" });
+ try {
+ expect(await readdir(directory)).toEqual([]);
+ await first.close();
+ // Closing one descriptor owner must not close another server/CLI owner's
+ // read admission. The empty private directory requires no live bridge.
+ expect((await first.listSessions()).ok).toBe(false);
+ expect(await second.listSessions()).toMatchObject({
+ ok: true,
+ value: {
+ bridge_directory: directory,
+ discovery_status: "no_live_bridge_found",
+ sessions: [],
+ },
+ });
+ expect(await readdir(directory)).toEqual([]);
+ } finally {
+ await Promise.all([first.close(), second.close()]);
+ }
+});
+
+it("keeps debugger lifetimes fresh and honors platform admission before any launch", async () => {
+ const spawnProcess = vi.fn(async () => {
+ throw new Error("A factory or unsupported host must not launch a debugger");
+ });
+ const gdb = [
+ createGdbSessionManager({}, { platform: "win32", spawnProcess }),
+ createGdbSessionManager({}, { platform: "win32", spawnProcess }),
+ ] as const;
+ const rizin = [
+ createRizinDebugSessionManager({}, { platform: "win32", spawnProcess }),
+ createRizinDebugSessionManager({}, { platform: "win32", spawnProcess }),
+ ] as const;
+ try {
+ expect(spawnProcess).not.toHaveBeenCalled();
+ await gdb[0].closeAll();
+ await rizin[0].closeAll();
+ expect(await gdb[0].start()).toMatchObject({
+ ok: false,
+ error: { _tag: "AnalysisCancelledError" },
+ });
+ expect(await gdb[1].start()).toMatchObject({
+ ok: false,
+ error: { _tag: "AnalysisCapabilityUnavailableError" },
+ });
+ const input = { path: "fixture", inferior_tty: "fixture-tty" };
+ expect(await rizin[0].start(input)).toMatchObject({
+ ok: false,
+ error: { _tag: "AnalysisInputError" },
+ });
+ expect(await rizin[1].start(input)).toMatchObject({
+ ok: false,
+ error: { _tag: "AnalysisCapabilityUnavailableError" },
+ });
+ expect(spawnProcess).not.toHaveBeenCalled();
+ } finally {
+ await Promise.all([...gdb, ...rizin].map((owner) => owner.closeAll()));
+ }
+});
diff --git a/tests/conformance/capabilityDeclarations.test.ts b/tests/conformance/capabilityDeclarations.test.ts
new file mode 100644
index 0000000000..0b71f93260
--- /dev/null
+++ b/tests/conformance/capabilityDeclarations.test.ts
@@ -0,0 +1,201 @@
+import { describe, expect, it } from "vitest";
+
+import { providerCatalog } from "../../scripts/lib/catalog-builders.mjs";
+import { ADB_PROVIDER_IDENTITY } from "../../src/adb/AdbProviderIdentity.js";
+import {
+ CAPABILITY_DECLARATIONS,
+ type CapabilityDeclaration,
+} from "../../src/composition/capabilityDeclarations.js";
+import { TOOL_CONTRACTS } from "../../src/contracts/toolContracts.js";
+import { TOOL_CONTRACT_GROUPS } from "../../src/contracts/toolFamilies.js";
+import { QLOG_CAPTURE_PROVIDER_IDENTITY } from "../../src/browser/history/CaptureProviderIdentities.js";
+import { HARMONY_APPLICATION_PROVIDER } from "../../src/domain/investigationProviders.js";
+import {
+ WEB_MODULE_TRACE_PROVIDER,
+ WEB_SOURCE_LOCATION_PROVIDER,
+} from "../../src/domain/webTraceProviders.js";
+
+const fixtureIdentity = {
+ id: "authored-fixture",
+ name: "Authored provider identity",
+ version: null,
+};
+const catalog = (
+ declarations: Parameters<
+ typeof providerCatalog
+ >[0]["capabilityDeclarations"]["CAPABILITY_DECLARATIONS"],
+) =>
+ providerCatalog({
+ toolContracts: { TOOL_CONTRACTS },
+ toolFamilies: { TOOL_CONTRACT_GROUPS },
+ capabilityDeclarations: { CAPABILITY_DECLARATIONS: declarations },
+ });
+
+describe("whole canonical contract groups", () => {
+ it("includes a new canonical ADB operation without editing actual provider metadata", () => {
+ const before = catalog(CAPABILITY_DECLARATIONS);
+ const adb = before.find(({ id }) => id === ADB_PROVIDER_IDENTITY.id);
+ expect(adb).toBeDefined();
+ if (adb === undefined) throw new Error("Missing actual ADB provider");
+ // Simulate the next real contract being added to its existing source group.
+ // The production declaration is deliberately unchanged, so a copied list
+ // would miss this valid operation even if every other provider used groups.
+ const added = { name: "new_valid_adb_operation" };
+ const after = providerCatalog({
+ capabilityDeclarations: { CAPABILITY_DECLARATIONS },
+ toolContracts: { TOOL_CONTRACTS: [...TOOL_CONTRACTS, added] },
+ toolFamilies: {
+ TOOL_CONTRACT_GROUPS: {
+ ...TOOL_CONTRACT_GROUPS,
+ ADB_TOOL_CONTRACTS: [
+ ...TOOL_CONTRACT_GROUPS.ADB_TOOL_CONTRACTS,
+ added,
+ ],
+ },
+ },
+ });
+ expect(after.find(({ id }) => id === adb.id)).toEqual({
+ ...adb,
+ capabilities: [...adb.capabilities, added.name].sort(),
+ });
+ expect(after.filter(({ id }) => id !== adb.id)).toEqual(
+ before.filter(({ id }) => id !== adb.id),
+ );
+ });
+
+ it("includes every canonical operation selected by real whole-group bindings", () => {
+ const providers = new Map(
+ catalog(CAPABILITY_DECLARATIONS).map((provider) => [
+ provider.id,
+ provider,
+ ]),
+ );
+ const declarations: readonly CapabilityDeclaration[] = Object.values(
+ CAPABILITY_DECLARATIONS,
+ );
+ let examined = 0;
+ for (const declaration of declarations)
+ for (const { identity, operations } of declaration.providers) {
+ if (!("group" in operations)) continue;
+ const expected = TOOL_CONTRACT_GROUPS[operations.group].map(
+ ({ name }) => name,
+ );
+ expect(providers.get(identity.id)?.capabilities).toEqual(
+ expect.arrayContaining(expected),
+ );
+ examined += 1;
+ }
+ expect(examined).toBeGreaterThan(0);
+ });
+});
+
+describe("static capability provider declarations", () => {
+ it("reports the actual trace, Harmony and qlog provenance without changing their public operations", () => {
+ const providers = catalog(CAPABILITY_DECLARATIONS);
+ for (const [identity, operation] of [
+ [WEB_SOURCE_LOCATION_PROVIDER, "trace_web_source_location"],
+ [WEB_MODULE_TRACE_PROVIDER, "trace_web_module_imports"],
+ [HARMONY_APPLICATION_PROVIDER, "project_harmony_application_graph"],
+ [QLOG_CAPTURE_PROVIDER_IDENTITY, "inspect_web_network_capture"],
+ ] as const)
+ expect(providers.find(({ id }) => id === identity.id)).toEqual({
+ ...identity,
+ capabilities: [operation],
+ });
+ expect(
+ providers.find(({ id }) => id === "rea-web-script-export")?.capabilities,
+ ).toEqual(["export_web_scripts"]);
+ expect(
+ providers.find(({ id }) => id === "rea-javascript-application-workflows")
+ ?.capabilities,
+ ).not.toContain("project_harmony_application_graph");
+ });
+
+ it("merges shared provider identities and operation unions independently of declaration order", () => {
+ const first = {
+ providers: [
+ {
+ identity: fixtureIdentity,
+ operations: ["inspect_wasm_artifact", "inspect_apk_signature"],
+ },
+ ],
+ };
+ const second = {
+ providers: [
+ {
+ identity: { ...fixtureIdentity },
+ operations: ["inspect_apk_signature", "inspect_evm_interface"],
+ },
+ ],
+ };
+ const expected = [
+ {
+ ...fixtureIdentity,
+ capabilities: [
+ "inspect_apk_signature",
+ "inspect_evm_interface",
+ "inspect_wasm_artifact",
+ ],
+ },
+ ];
+ expect(catalog({ first, second, files: { providers: [] } })).toEqual(
+ expected,
+ );
+ expect(catalog({ second, first })).toEqual(expected);
+ });
+
+ it.each([
+ { name: "A different provider name" },
+ { version: "a different release" },
+ ])(
+ "refuses conflicting identities sharing the same provider ID: %j",
+ (change) => {
+ expect(() =>
+ catalog({
+ first: {
+ providers: [{ identity: fixtureIdentity, operations: [] }],
+ },
+ second: {
+ providers: [
+ { identity: { ...fixtureIdentity, ...change }, operations: [] },
+ ],
+ },
+ }),
+ ).toThrow("Provider authored-fixture has conflicting source identities");
+ },
+ );
+
+ it("refuses an unknown analyst operation instead of silently dropping the provider binding", () => {
+ expect(() =>
+ catalog({
+ fixture: {
+ providers: [
+ {
+ identity: fixtureIdentity,
+ operations: ["not_an_advertised_operation"],
+ },
+ ],
+ },
+ }),
+ ).toThrow(
+ "Provider authored-fixture declares an unknown analyst operation: not_an_advertised_operation",
+ );
+ });
+
+ it("refuses an unknown canonical contract group", () => {
+ expect(() =>
+ catalog({
+ fixture: {
+ providers: [
+ {
+ identity: fixtureIdentity,
+ operations: { group: "not_a_contract_group" },
+ },
+ ],
+ },
+ }),
+ ).toThrow(
+ "Provider authored-fixture declares an unknown contract group: not_a_contract_group",
+ );
+ });
+});
diff --git a/tests/conformance/ida/providerContracts.test.ts b/tests/conformance/ida/providerContracts.test.ts
index ae2599c5ea..bfa4044d25 100644
--- a/tests/conformance/ida/providerContracts.test.ts
+++ b/tests/conformance/ida/providerContracts.test.ts
@@ -4,10 +4,8 @@ import { rm } from "node:fs/promises";
import { afterEach, describe, expect, it } from "vitest";
import { createIdaTarget, RecordingIdaMcp } from "../../fixtures/idaMcp.js";
import { IdaSessionClient } from "../../../src/ida/IdaSessionClient.js";
-import {
- IDA_OPERATIONS,
- idaCapabilities,
-} from "../../../src/ida/IdaProviderCapabilities.js";
+import { idaCapabilities } from "../../../src/ida/IdaProviderCapabilities.js";
+import { IDA_OPERATIONS } from "../../../src/ida/IdaProviderMetadata.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
diff --git a/tests/conformance/verifierMetadataLinks.test.ts b/tests/conformance/verifierMetadataLinks.test.ts
new file mode 100644
index 0000000000..594f820ece
--- /dev/null
+++ b/tests/conformance/verifierMetadataLinks.test.ts
@@ -0,0 +1,239 @@
+import { parse } from "@babel/parser";
+import * as t from "@babel/types";
+import { readFile, readdir } from "node:fs/promises";
+import { dirname, join, relative, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import { expect, it } from "vitest";
+
+import {
+ importSpecifier,
+ readImportSpecifiers,
+} from "../../scripts/lib/import-specifiers.mjs";
+
+const root = fileURLToPath(new URL("../../", import.meta.url));
+const syntax = (source: string) =>
+ parse(source, { sourceType: "module", plugins: ["typescript"] });
+
+const exportedValues = (tree: ReturnType) => {
+ const names = new Set();
+ for (const node of tree.program.body) {
+ if (!t.isExportNamedDeclaration(node) || node.exportKind === "type")
+ continue;
+ const declaration = node.declaration;
+ if (t.isVariableDeclaration(declaration)) {
+ for (const binding of declaration.declarations)
+ if (t.isIdentifier(binding.id)) names.add(binding.id.name);
+ } else if (
+ (t.isFunctionDeclaration(declaration) ||
+ t.isClassDeclaration(declaration)) &&
+ declaration.id != null
+ )
+ names.add(declaration.id.name);
+ for (const binding of node.specifiers)
+ if (!t.isExportSpecifier(binding) || binding.exportKind !== "type")
+ names.add(
+ t.isIdentifier(binding.exported)
+ ? binding.exported.name
+ : binding.exported.value,
+ );
+ }
+ return names;
+};
+
+/** Derive migrated metadata symbols from their current source owners. */
+const metadataSymbols = async () => {
+ const pending = [resolve(root, "src/composition/capabilityDeclarations.ts")];
+ const seen = new Set();
+ const symbols = new Set();
+ while (pending.length > 0) {
+ const file = pending.pop();
+ if (file === undefined || seen.has(file)) continue;
+ seen.add(file);
+ const source = await readFile(file, "utf8");
+ for (const name of exportedValues(syntax(source))) symbols.add(name);
+ for (const fact of readImportSpecifiers(source))
+ if (!fact.typeOnly && fact.specifier.startsWith("."))
+ pending.push(
+ resolve(dirname(file), fact.specifier).replace(/\.js$/u, ".ts"),
+ );
+ }
+ return symbols;
+};
+
+const literal = (node: t.Node | null | undefined): string | undefined =>
+ t.isStringLiteral(node)
+ ? node.value
+ : t.isTemplateLiteral(node) && node.expressions.length === 0
+ ? (node.quasis[0]?.value.cooked ?? undefined)
+ : undefined;
+
+/** Interpret only the literal/fileURL/join patterns used by packaged verifiers. */
+const compiledReference = (
+ node: t.Node | null | undefined,
+): string | undefined => {
+ const value = literal(node);
+ if (value !== undefined) return value;
+ if (!t.isCallExpression(node) || !t.isIdentifier(node.callee))
+ return undefined;
+ if (node.callee.name === "pathToFileURL")
+ return compiledReference(node.arguments[0]);
+ if (node.callee.name === "join")
+ return node.arguments
+ .map(literal)
+ .find((part) => part?.startsWith("dist/"));
+ return undefined;
+};
+
+const dynamicSource = (node: t.Node | null | undefined): t.Node | undefined => {
+ if (t.isAwaitExpression(node)) return dynamicSource(node.argument);
+ if (t.isImportExpression(node)) return node.source;
+ return undefined;
+};
+
+interface MetadataLink {
+ readonly name: string;
+ readonly reference: string | undefined;
+ readonly line: number;
+}
+
+/** Static linking only: no verifier main or provider implementation is executed. */
+const metadataLinks = (source: string, names: ReadonlySet) => {
+ const tree = syntax(source);
+ const links: MetadataLink[] = [];
+ const namespaces = new Map();
+ const add = (name: string, reference: string | undefined, node: t.Node) => {
+ if (names.has(name))
+ links.push({ name, reference, line: node.loc?.start.line ?? 1 });
+ };
+ t.traverseFast(tree, (node) => {
+ if (t.isImportDeclaration(node) && node.importKind !== "type") {
+ const reference = importSpecifier(node);
+ for (const binding of node.specifiers) {
+ if (t.isImportSpecifier(binding) && binding.importKind !== "type")
+ add(
+ t.isIdentifier(binding.imported)
+ ? binding.imported.name
+ : binding.imported.value,
+ reference,
+ node,
+ );
+ else if (t.isImportNamespaceSpecifier(binding))
+ namespaces.set(binding.local.name, reference);
+ }
+ }
+ if (!t.isVariableDeclarator(node)) return;
+ const target = dynamicSource(node.init);
+ if (target === undefined) return;
+ const reference = compiledReference(target);
+ if (t.isIdentifier(node.id)) namespaces.set(node.id.name, reference);
+ if (t.isObjectPattern(node.id))
+ for (const property of node.id.properties)
+ if (t.isObjectProperty(property)) {
+ const name = t.isIdentifier(property.key)
+ ? property.key.name
+ : literal(property.key);
+ if (name !== undefined) add(name, reference, node);
+ }
+ });
+ t.traverseFast(tree, (node) => {
+ if (
+ !t.isMemberExpression(node) ||
+ !t.isIdentifier(node.object) ||
+ !namespaces.has(node.object.name)
+ )
+ return;
+ const name =
+ !node.computed && t.isIdentifier(node.property)
+ ? node.property.name
+ : literal(node.property);
+ if (name !== undefined) add(name, namespaces.get(node.object.name), node);
+ });
+ return links;
+};
+
+const scriptsBelow = async (directory: string): Promise => {
+ const entries = await readdir(directory, { withFileTypes: true });
+ const nested = await Promise.all(
+ entries.map((entry) => {
+ const path = join(directory, entry.name);
+ return entry.isDirectory()
+ ? scriptsBelow(path)
+ : Promise.resolve(/\.(?:mjs|js)$/u.test(path) ? [path] : []);
+ }),
+ );
+ return nested.flat();
+};
+
+const linkFailures = async (
+ file: string,
+ source: string,
+ symbols: ReadonlySet,
+) => {
+ const failures: string[] = [];
+ const links = metadataLinks(source, symbols);
+ for (const { name, reference, line } of links) {
+ if (reference === undefined) {
+ failures.push(
+ `${relative(root, file)}:${line}: unresolved ${name} import`,
+ );
+ continue;
+ }
+ const target = reference.startsWith("file:")
+ ? fileURLToPath(reference)
+ : reference.startsWith("dist/")
+ ? resolve(root, reference)
+ : resolve(dirname(file), reference);
+ const compiled = relative(root, target).replaceAll("\\", "/");
+ if (!compiled.startsWith("dist/")) continue;
+ const sourcePath = resolve(
+ root,
+ compiled.replace(/^dist\//u, "src/"),
+ ).replace(/\.js$/u, ".ts");
+ const exports = exportedValues(syntax(await readFile(sourcePath, "utf8")));
+ if (!exports.has(name))
+ failures.push(
+ `${relative(root, file)}:${line}: ${compiled} has no ${name} export`,
+ );
+ }
+ return { failures, checked: links.length };
+};
+
+it("links verifier metadata exports through static, namespace and packaged dynamic imports", async () => {
+ const symbols = await metadataSymbols();
+ const files = [
+ ...(await scriptsBelow(resolve(root, "scripts"))),
+ ...(await scriptsBelow(resolve(root, "tests/fixtures"))),
+ ];
+ const failures: string[] = [];
+ let checked = 0;
+ for (const file of files) {
+ const result = await linkFailures(
+ file,
+ await readFile(file, "utf8"),
+ symbols,
+ );
+ failures.push(...result.failures);
+ checked += result.checked;
+ }
+ expect(checked).toBeGreaterThan(0);
+ expect(failures).toEqual([]);
+});
+
+it.each([
+ 'import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js";',
+ 'const { GHIDRA_OPERATIONS } = await import(pathToFileURL(join(packageRoot, "dist/ghidra/GhidraProviderCapabilities.js")));',
+ 'const metadata = await import("../dist/ghidra/GhidraProviderCapabilities.js"); metadata.GHIDRA_OPERATIONS;',
+])(
+ "detects a metadata consumer whose old owner no longer exports its requirement: %s",
+ async (source) => {
+ const result = await linkFailures(
+ resolve(root, "scripts/authored-negative-control.mjs"),
+ source,
+ await metadataSymbols(),
+ );
+ expect(result.failures).toHaveLength(1);
+ expect(result.failures[0]).toMatch(
+ / has no (?:HOPPER_PROVIDER_IDENTITY|GHIDRA_OPERATIONS) export$/u,
+ );
+ },
+);
diff --git a/tests/evaluation/importSpecifiers.test.ts b/tests/evaluation/importSpecifiers.test.ts
index db7225d73b..067aec1738 100644
--- a/tests/evaluation/importSpecifiers.test.ts
+++ b/tests/evaluation/importSpecifiers.test.ts
@@ -1,10 +1,26 @@
import { describe, expect, it } from "vitest";
+import { fileURLToPath } from "node:url";
+import ts from "typescript6";
import {
importSpecifier,
readImportSpecifiers,
} from "../../scripts/lib/import-specifiers.mjs";
+// Convert only compiler options; this does not discover or compile the project.
+const configuration = ts.readConfigFile(
+ fileURLToPath(new URL("../../tsconfig.json", import.meta.url)),
+ ts.sys.readFile.bind(ts.sys),
+);
+if (configuration.error !== undefined)
+ throw new Error("Could not read repository compiler options");
+const compilerOptions = ts.convertCompilerOptionsFromJson(
+ configuration.config.compilerOptions,
+ fileURLToPath(new URL("../../", import.meta.url)),
+);
+if (compilerOptions.errors.length > 0)
+ throw new Error("Could not convert repository compiler options");
+
describe("import specifier reader", () => {
it("reads side-effect imports and ignores import-like text", () => {
expect(
@@ -35,15 +51,55 @@ const computed = import(name);
import "node:fs";
`);
expect(facts).toEqual([
- { specifier: "./types.js", line: 1 },
- { specifier: "./all.js", line: 2 },
- { specifier: "./equals.js", line: 3 },
- { specifier: "./query.js", line: 4 },
- { specifier: "./template.js", line: 5 },
- { specifier: "node:fs", line: 7 },
+ { specifier: "./types.js", line: 1, typeOnly: true },
+ { specifier: "./all.js", line: 2, typeOnly: false },
+ { specifier: "./equals.js", line: 3, typeOnly: false },
+ { specifier: "./query.js", line: 4, typeOnly: true },
+ { specifier: "./template.js", line: 5, typeOnly: false },
+ { specifier: "node:fs", line: 7, typeOnly: false },
]);
});
+ it.each([
+ ['import type Default from "./owner.js";', true],
+ ['import type * as Namespace from "./owner.js";', true],
+ ['import { type Port, type Other as Alias } from "./owner.js";', false],
+ ['import { type Port, Constructor as Alias } from "./owner.js";', false],
+ ['import Default, { type Port } from "./owner.js";', false],
+ ['import {} from "./owner.js";', false],
+ ['import "./owner.js";', false],
+ ['export type { Port } from "./owner.js";', true],
+ ['export { type Port, type Other as Alias } from "./owner.js";', false],
+ ['export { type Port, Constructor as Alias } from "./owner.js";', false],
+ ['export {} from "./owner.js";', false],
+ ['export type * from "./owner.js";', true],
+ ['export type * as Namespace from "./owner.js";', true],
+ ['export * as Namespace from "./owner.js";', false],
+ ['import type Owner = require("./owner.js");', true],
+ ['import Owner = require("./owner.js");', false],
+ ['type Port = import("./owner.js").Port;', true],
+ ['type Constructor = typeof import("./owner.js").Constructor;', true],
+ ['const Owner = import("./owner.js");', false],
+ ["const Owner = import(`./owner.js`);", false],
+ ])("matches TypeScript's emitted runtime edge for %s", (source, typeOnly) => {
+ expect(readImportSpecifiers(source)).toEqual([
+ { specifier: "./owner.js", line: 1, typeOnly },
+ ]);
+ expect(compilerOptions.options.verbatimModuleSyntax).toBe(true);
+ const emitted = ts.transpileModule(source, {
+ compilerOptions: compilerOptions.options,
+ // NodeNext emits import-equals through createRequire in an ESM file.
+ fileName: fileURLToPath(new URL("./import-owner.mts", import.meta.url)),
+ reportDiagnostics: true,
+ });
+ expect(
+ emitted.diagnostics?.filter(
+ (diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error,
+ ),
+ ).toEqual([]);
+ expect(emitted.outputText.includes("./owner.js")).toBe(!typeOnly);
+ });
+
it("ignores nodes that are not static imports", () => {
expect(importSpecifier({ type: "Identifier" })).toBeUndefined();
});
diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts
index 15e6c0d0d7..75f0bd4f4c 100644
--- a/tests/evaluation/moduleBoundaries.test.ts
+++ b/tests/evaluation/moduleBoundaries.test.ts
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
import { inspectModuleBoundaries } from "../../scripts/lib/module-boundaries.mjs";
+import { projectForTest } from "../../scripts/lib/test-projects.mjs";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { writeRepositoryOxlintConfiguration } from "../fixtures/oxlintConfiguration.js";
@@ -65,6 +66,113 @@ describe("binary production construction ownership", () => {
});
});
+describe("live capability constructor ownership", () => {
+ it.each([
+ "frida/FridaInstrumentationManager.js",
+ "cutter/CutterBridgeClient.js",
+ "cutter/CutterBridgeService.js",
+ "gdb/GdbSessionManager.js",
+ "rizin/RizinDebugSessionManager.js",
+ ])(
+ "keeps %s values in composition while accepting injected type ports",
+ (owner) => {
+ const source = `import { Constructor as Alias } from "../${owner}";`;
+ const types = `import type { Port } from "../${owner}";`;
+ for (const file of ["src/application/probe.ts", "src/server/probe.ts"]) {
+ expect(
+ inspectModuleBoundaries(file, source, process.cwd()),
+ ).toMatchObject([{ boundary: "provider-construction" }]);
+ expect(inspectModuleBoundaries(file, types, process.cwd())).toEqual([]);
+ }
+ expect(
+ inspectModuleBoundaries(
+ "src/composition/probe.ts",
+ source,
+ process.cwd(),
+ ),
+ ).toEqual([]);
+ },
+ );
+
+ it.each([
+ [
+ 'import { Constructor as Alias } from "../frida/FridaInstrumentationManager.js";',
+ false,
+ ],
+ [
+ 'import * as Owner from "../frida/FridaInstrumentationManager.js";',
+ false,
+ ],
+ [
+ 'import { type Port, Constructor } from "../frida/FridaInstrumentationManager.js";',
+ false,
+ ],
+ [
+ 'import { type Port as Alias } from "../frida/FridaInstrumentationManager.js";',
+ false,
+ ],
+ ['import "../frida/FridaInstrumentationManager.js";', false],
+ [
+ 'export { Constructor as Alias } from "../frida/FridaInstrumentationManager.js";',
+ false,
+ ],
+ [
+ 'export { type Port as Alias } from "../frida/FridaInstrumentationManager.js";',
+ false,
+ ],
+ ['export type * from "../frida/FridaInstrumentationManager.js";', true],
+ ['export * from "../frida/FridaInstrumentationManager.js";', false],
+ [
+ 'type Port = import("../frida/FridaInstrumentationManager.js").Port;',
+ true,
+ ],
+ ['const owner = import("../frida/FridaInstrumentationManager.js");', false],
+ ["const owner = import(`../frida/FridaInstrumentationManager.js`);", false],
+ [
+ 'import owner = require("../frida/FridaInstrumentationManager.js");',
+ false,
+ ],
+ [
+ 'import type Owner = require("../frida/FridaInstrumentationManager.js");',
+ true,
+ ],
+ [
+ 'import { Constructor } from "../domain/../frida/FridaInstrumentationManager.js";',
+ false,
+ ],
+ [
+ 'const owner = import("../%66rida/FridaInstrumentationManager.js?view=one#part");',
+ false,
+ ],
+ ])("checks type/value ownership in %s", (source, allowed) => {
+ const violations = inspectModuleBoundaries(
+ "src/server/probe.ts",
+ source,
+ process.cwd(),
+ );
+ if (allowed) expect(violations).toEqual([]);
+ else
+ expect(violations).toMatchObject([{ boundary: "provider-construction" }]);
+ });
+
+ it("does not broaden the new guard to other provider-owned policy helpers", () => {
+ expect(
+ inspectModuleBoundaries(
+ "src/server/probe.ts",
+ 'import { policy } from "../frida/fridaCaptureLimits.js";',
+ process.cwd(),
+ ),
+ ).toEqual([]);
+ });
+
+ it.each([
+ "src/frida/FridaInstrumentationManager.test.ts",
+ "src/frida/nested/FutureProvider.test.ts",
+ ])("assigns %s to the adapter test project", (path) => {
+ expect(projectForTest(path).name).toBe("adapters");
+ });
+});
+
const execute = promisify(execFile);
const verifier = resolve("scripts/verify-module-boundaries.mjs");
diff --git a/tests/process-global/historicalCaptureSnapshotRedaction.test.ts b/tests/process-global/historicalCaptureSnapshotRedaction.test.ts
index ff195957f4..f678533ad1 100644
--- a/tests/process-global/historicalCaptureSnapshotRedaction.test.ts
+++ b/tests/process-global/historicalCaptureSnapshotRedaction.test.ts
@@ -33,7 +33,7 @@ vi.mock("node:fs/promises", async (importOriginal) => {
});
import { HistoricalCaptureDecoder } from "../../src/browser/history/HistoricalCaptureDecoder.js";
-import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../src/browser/history/CaptureRelease.js";
+import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../src/browser/history/CaptureProviderIdentities.js";
import { WebNetworkCaptureService } from "../../src/application/WebNetworkCaptureService.js";
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
import { fileSnapshotPartialObservationSchema } from "../../src/domain/fileSnapshotObservation.js";
diff --git a/tests/process-global/historicalCaptureSourceSnapshots.test.ts b/tests/process-global/historicalCaptureSourceSnapshots.test.ts
index 3010d6cc09..1f1575e6ae 100644
--- a/tests/process-global/historicalCaptureSourceSnapshots.test.ts
+++ b/tests/process-global/historicalCaptureSourceSnapshots.test.ts
@@ -59,7 +59,7 @@ vi.mock("node:fs/promises", async (importOriginal) => {
});
import { HistoricalCaptureDecoder } from "../../src/browser/history/HistoricalCaptureDecoder.js";
-import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../src/browser/history/CaptureRelease.js";
+import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../src/browser/history/CaptureProviderIdentities.js";
import { decodeHarCapture } from "../../src/browser/history/HarCapture.js";
import { inspectWebNetworkCaptureInputSchema } from "../../src/domain/webNetworkCapture.js";
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
diff --git a/tests/process-global/pwntoolsCliFinalization.test.ts b/tests/process-global/pwntoolsCliFinalization.test.ts
index 97c0c09f0f..6f264db5fe 100644
--- a/tests/process-global/pwntoolsCliFinalization.test.ts
+++ b/tests/process-global/pwntoolsCliFinalization.test.ts
@@ -18,7 +18,7 @@ import { resolvedAnalysisError } from "../../src/domain/analysisErrorBase.js";
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
import { PwntoolsLayoutProvider } from "../../src/native/pwntools/PwntoolsLayoutProvider.js";
import type { PwntoolsLauncher } from "../../src/native/pwntools/PwntoolsDecoder.js";
-import { PWNTOOLS_PROVIDER_IDENTITY } from "../../src/native/pwntools/PwntoolsRelease.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "../../src/native/pwntools/PwntoolsProviderIdentity.js";
import { PrivateRuntimeRoot } from "../../src/process/PrivateRuntimeRoot.js";
import { cleanupOwnedProcessGroup } from "../../src/process/ProcessOwnership.js";
import { prepareProcessOwnershipInspection } from "../../src/process/ProcessOwnershipObservation.js";
diff --git a/tests/process-global/pwntoolsStageRetirement.test.ts b/tests/process-global/pwntoolsStageRetirement.test.ts
index 1ea6b0a146..3e48a4e10d 100644
--- a/tests/process-global/pwntoolsStageRetirement.test.ts
+++ b/tests/process-global/pwntoolsStageRetirement.test.ts
@@ -23,10 +23,8 @@ import { primaryAnalysisError } from "../../src/domain/analysisErrorBase.js";
import { ProviderCleanupError } from "../../src/domain/providerCleanupError.js";
import { PwntoolsRecordedCrashProvider } from "../../src/native/pwntools/PwntoolsRecordedCrashProvider.js";
import type { PwntoolsLauncher } from "../../src/native/pwntools/PwntoolsDecoder.js";
-import {
- PWNTOOLS_MEMORY_FAILURE_EXIT,
- PWNTOOLS_PROVIDER_IDENTITY,
-} from "../../src/native/pwntools/PwntoolsRelease.js";
+import { PWNTOOLS_MEMORY_FAILURE_EXIT } from "../../src/native/pwntools/PwntoolsRelease.js";
+import { PWNTOOLS_PROVIDER_IDENTITY } from "../../src/native/pwntools/PwntoolsProviderIdentity.js";
import {
cleanupOwnedProcessGroup,
type OwnedProcessGroup,